@aj-archipelago/cortex 1.3.65 → 1.3.67

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.
Files changed (35) hide show
  1. package/helper-apps/cortex-autogen2/Dockerfile +88 -21
  2. package/helper-apps/cortex-autogen2/docker-compose.yml +15 -8
  3. package/helper-apps/cortex-autogen2/host.json +5 -0
  4. package/helper-apps/cortex-autogen2/pyproject.toml +82 -25
  5. package/helper-apps/cortex-autogen2/requirements.txt +84 -14
  6. package/helper-apps/cortex-autogen2/services/redis_publisher.py +129 -3
  7. package/helper-apps/cortex-autogen2/task_processor.py +432 -116
  8. package/helper-apps/cortex-autogen2/tools/__init__.py +2 -0
  9. package/helper-apps/cortex-autogen2/tools/azure_blob_tools.py +32 -0
  10. package/helper-apps/cortex-autogen2/tools/azure_foundry_agents.py +50 -14
  11. package/helper-apps/cortex-autogen2/tools/file_tools.py +169 -44
  12. package/helper-apps/cortex-autogen2/tools/google_cse.py +117 -0
  13. package/helper-apps/cortex-autogen2/tools/search_tools.py +655 -98
  14. package/lib/entityConstants.js +1 -1
  15. package/lib/pathwayManager.js +42 -8
  16. package/lib/pathwayTools.js +3 -3
  17. package/lib/util.js +58 -2
  18. package/package.json +1 -1
  19. package/pathways/system/entity/memory/sys_memory_format.js +1 -0
  20. package/pathways/system/entity/memory/sys_memory_manager.js +3 -3
  21. package/pathways/system/entity/sys_entity_start.js +1 -1
  22. package/pathways/system/entity/tools/sys_tool_bing_search_afagent.js +2 -0
  23. package/pathways/system/entity/tools/sys_tool_codingagent.js +2 -2
  24. package/pathways/system/entity/tools/sys_tool_google_search.js +3 -3
  25. package/pathways/system/entity/tools/sys_tool_grok_x_search.js +12 -2
  26. package/pathways/system/workspaces/run_workspace_prompt.js +0 -3
  27. package/server/executeWorkspace.js +381 -0
  28. package/server/graphql.js +5 -180
  29. package/server/pathwayResolver.js +3 -3
  30. package/server/plugins/apptekTranslatePlugin.js +2 -2
  31. package/server/plugins/azureFoundryAgentsPlugin.js +1 -1
  32. package/tests/unit/core/parser.test.js +0 -1
  33. package/tests/unit/core/pathwayManagerWithFiles.test.js +256 -0
  34. package/tests/unit/graphql_executeWorkspace_transformation.test.js +244 -0
  35. package/tests/unit/server/graphql.test.js +122 -1
@@ -0,0 +1,256 @@
1
+ /**
2
+ * PathwayManager File Handling Tests
3
+ *
4
+ * This test suite validates the PathwayManager's file handling functionality, specifically
5
+ * testing how the manager processes and transforms pathway prompts that include file attachments.
6
+ *
7
+ * Key functionality tested:
8
+ * - File hash transformation from 'files' to 'fileHashes' property
9
+ * - Collection and deduplication of file hashes at the pathway level
10
+ * - Backward compatibility with legacy string-based prompts without file attachments
11
+ * - Handling of edge cases (null, undefined, empty file arrays)
12
+ * - Prompt object creation with file metadata
13
+ *
14
+ * The PathwayManager allows prompts to reference files by their hashes, which are then
15
+ * processed and made available to the execution context. This test suite ensures that
16
+ * file metadata is correctly preserved, transformed, and aggregated during pathway processing.
17
+ *
18
+ * Test scenarios covered:
19
+ * 1. Prompts with multiple file attachments
20
+ * 2. Prompts with empty or missing file arrays
21
+ * 3. Legacy string prompts (no file support)
22
+ * 4. Duplicate file hash deduplication
23
+ * 5. Null/undefined file handling
24
+ * 6. Direct prompt object creation with files
25
+ */
26
+
27
+ import test from 'ava';
28
+ import PathwayManager from '../../../lib/pathwayManager.js';
29
+
30
+ // Mock config for PathwayManager
31
+ const mockConfig = {
32
+ storageType: 'local',
33
+ filePath: './test-pathways.json',
34
+ publishKey: 'test-key'
35
+ };
36
+
37
+ // Mock base pathway
38
+ const mockBasePathway = {
39
+ name: 'base',
40
+ prompt: '{{text}}',
41
+ systemPrompt: '',
42
+ inputParameters: {},
43
+ typeDef: 'type Test { test: String }',
44
+ rootResolver: () => {},
45
+ resolver: () => {}
46
+ };
47
+
48
+ // Mock storage strategy
49
+ class MockStorageStrategy {
50
+ async load() {
51
+ return {};
52
+ }
53
+
54
+ async save(data) {
55
+ // Do nothing
56
+ }
57
+
58
+ async getLastModified() {
59
+ return Date.now();
60
+ }
61
+ }
62
+
63
+ test('pathwayManager handles prompt format with files correctly', async t => {
64
+ const pathwayManager = new PathwayManager(mockConfig, mockBasePathway);
65
+
66
+ // Replace storage with mock
67
+ pathwayManager.storage = new MockStorageStrategy();
68
+
69
+ // Test prompts with files
70
+ const pathwayWithFiles = {
71
+ prompt: [
72
+ {
73
+ name: 'Analyze Document',
74
+ prompt: 'Please analyze the provided document',
75
+ files: ['abc123def456', 'def456ghi789']
76
+ },
77
+ {
78
+ name: 'Summarize Text',
79
+ prompt: 'Please summarize the text',
80
+ files: []
81
+ },
82
+ {
83
+ name: 'Simple Task',
84
+ prompt: 'Perform a simple task'
85
+ // No files property
86
+ }
87
+ ],
88
+ systemPrompt: 'You are a helpful assistant'
89
+ };
90
+
91
+ // Test transformPrompts method
92
+ const transformedPathway = await pathwayManager.transformPrompts(pathwayWithFiles);
93
+
94
+ // Verify the transformed pathway structure
95
+ t.truthy(transformedPathway);
96
+ t.true(Array.isArray(transformedPathway.prompt));
97
+ t.is(transformedPathway.prompt.length, 3);
98
+
99
+ // Verify first prompt with files
100
+ const firstPrompt = transformedPathway.prompt[0];
101
+ t.is(firstPrompt.name, 'Analyze Document');
102
+ t.truthy(firstPrompt.fileHashes);
103
+ t.deepEqual(firstPrompt.fileHashes, ['abc123def456', 'def456ghi789']);
104
+
105
+ // Verify second prompt with empty files
106
+ const secondPrompt = transformedPathway.prompt[1];
107
+ t.is(secondPrompt.name, 'Summarize Text');
108
+ t.falsy(secondPrompt.fileHashes); // Empty array results in no fileHashes property
109
+
110
+ // Verify third prompt without files property
111
+ const thirdPrompt = transformedPathway.prompt[2];
112
+ t.is(thirdPrompt.name, 'Simple Task');
113
+ t.falsy(thirdPrompt.fileHashes);
114
+
115
+ // Verify pathway-level file hashes collection
116
+ t.truthy(transformedPathway.fileHashes);
117
+ t.deepEqual(transformedPathway.fileHashes, ['abc123def456', 'def456ghi789']);
118
+ });
119
+
120
+ test('pathwayManager handles legacy string prompts correctly', async t => {
121
+ const pathwayManager = new PathwayManager(mockConfig, mockBasePathway);
122
+
123
+ // Replace storage with mock
124
+ pathwayManager.storage = new MockStorageStrategy();
125
+
126
+ // Test legacy string prompts
127
+ const legacyPathway = {
128
+ prompt: [
129
+ 'Please analyze the data',
130
+ 'Summarize the findings'
131
+ ],
132
+ systemPrompt: 'You are a helpful assistant'
133
+ };
134
+
135
+ // Test transformPrompts method
136
+ const transformedPathway = await pathwayManager.transformPrompts(legacyPathway);
137
+
138
+ // Verify the transformed pathway structure
139
+ t.truthy(transformedPathway);
140
+ t.true(Array.isArray(transformedPathway.prompt));
141
+ t.is(transformedPathway.prompt.length, 2);
142
+
143
+ // Verify prompts don't have file hashes
144
+ transformedPathway.prompt.forEach(prompt => {
145
+ t.falsy(prompt.fileHashes);
146
+ });
147
+
148
+ // Verify no pathway-level file hashes
149
+ t.falsy(transformedPathway.fileHashes);
150
+ });
151
+
152
+ test('pathwayManager removes duplicate file hashes at pathway level', async t => {
153
+ const pathwayManager = new PathwayManager(mockConfig, mockBasePathway);
154
+
155
+ // Replace storage with mock
156
+ pathwayManager.storage = new MockStorageStrategy();
157
+
158
+ // Test prompts with duplicate file hashes
159
+ const pathwayWithDuplicateFiles = {
160
+ prompt: [
161
+ {
162
+ name: 'First Task',
163
+ prompt: 'Analyze document 1',
164
+ files: ['abc123def456', 'def456ghi789']
165
+ },
166
+ {
167
+ name: 'Second Task',
168
+ prompt: 'Analyze document 2',
169
+ files: ['abc123def456', 'ghi789jkl012'] // abc123def456 is duplicate
170
+ }
171
+ ],
172
+ systemPrompt: 'You are a helpful assistant'
173
+ };
174
+
175
+ // Test transformPrompts method
176
+ const transformedPathway = await pathwayManager.transformPrompts(pathwayWithDuplicateFiles);
177
+
178
+ // Verify pathway-level file hashes are deduplicated
179
+ t.truthy(transformedPathway.fileHashes);
180
+ t.deepEqual(transformedPathway.fileHashes, ['abc123def456', 'def456ghi789', 'ghi789jkl012']);
181
+ t.is(transformedPathway.fileHashes.length, 3); // Should not have duplicates
182
+ });
183
+
184
+ test('pathwayManager handles null and undefined files gracefully', async t => {
185
+ const pathwayManager = new PathwayManager(mockConfig, mockBasePathway);
186
+
187
+ // Replace storage with mock
188
+ pathwayManager.storage = new MockStorageStrategy();
189
+
190
+ // Test prompts with null/undefined files
191
+ const pathwayWithNullFiles = {
192
+ prompt: [
193
+ {
194
+ name: 'Task with null files',
195
+ prompt: 'Do something',
196
+ files: null
197
+ },
198
+ {
199
+ name: 'Task with undefined files',
200
+ prompt: 'Do something else'
201
+ // files property is undefined
202
+ }
203
+ ],
204
+ systemPrompt: 'You are a helpful assistant'
205
+ };
206
+
207
+ // Test transformPrompts method
208
+ const transformedPathway = await pathwayManager.transformPrompts(pathwayWithNullFiles);
209
+
210
+ // Verify the transformation handles null/undefined gracefully
211
+ t.truthy(transformedPathway);
212
+ t.true(Array.isArray(transformedPathway.prompt));
213
+
214
+ // Both prompts should have empty or undefined fileHashes
215
+ transformedPathway.prompt.forEach(prompt => {
216
+ t.true(!prompt.fileHashes || prompt.fileHashes.length === 0);
217
+ });
218
+
219
+ // No pathway-level file hashes should be set
220
+ t.falsy(transformedPathway.fileHashes);
221
+ });
222
+
223
+ test('pathwayManager _createPromptObject handles files correctly', t => {
224
+ const pathwayManager = new PathwayManager(mockConfig, mockBasePathway);
225
+
226
+ // Test with object prompt containing files
227
+ const promptWithFiles = {
228
+ name: 'Test Prompt',
229
+ prompt: 'Analyze this document',
230
+ files: ['file1hash', 'file2hash']
231
+ };
232
+
233
+ const createdPrompt = pathwayManager._createPromptObject(promptWithFiles, 'System prompt');
234
+
235
+ t.is(createdPrompt.name, 'Test Prompt');
236
+ t.truthy(createdPrompt.fileHashes);
237
+ t.deepEqual(createdPrompt.fileHashes, ['file1hash', 'file2hash']);
238
+
239
+ // Test with string prompt (no files)
240
+ const stringPrompt = 'Simple text prompt';
241
+ const createdStringPrompt = pathwayManager._createPromptObject(stringPrompt, 'System prompt', 'Default Name');
242
+
243
+ t.is(createdStringPrompt.name, 'Default Name');
244
+ t.falsy(createdStringPrompt.fileHashes);
245
+
246
+ // Test with object prompt without files
247
+ const promptWithoutFiles = {
248
+ name: 'No Files Prompt',
249
+ prompt: 'Simple task'
250
+ };
251
+
252
+ const createdPromptNoFiles = pathwayManager._createPromptObject(promptWithoutFiles, 'System prompt');
253
+
254
+ t.is(createdPromptNoFiles.name, 'No Files Prompt');
255
+ t.falsy(createdPromptNoFiles.fileHashes);
256
+ });
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Cortex Pathway Argument Transformation Tests
3
+ *
4
+ * This test suite validates the argument transformation logic used when executing
5
+ * cortex pathways through the GraphQL executeWorkspace mutation. Specifically, it tests
6
+ * how the system transforms incoming pathway arguments (text, chatHistory, model) into
7
+ * the format expected by cortex pathways.
8
+ *
9
+ * Key transformation behaviors tested:
10
+ * - Merging text parameters with existing chatHistory entries
11
+ * - Preserving multimodal content (images, etc.) in the correct order
12
+ * - Creating new user messages when no chatHistory exists
13
+ * - Handling model selection with fallback to default values
14
+ * - Ensuring content arrays maintain proper JSON-stringified structure
15
+ *
16
+ * The transformation logic ensures that:
17
+ * 1. Text content is prepended to the last user message's content array
18
+ * 2. Existing multimodal content (e.g., images) is preserved
19
+ * 3. If no user message exists, a new one is created with the text
20
+ * 4. Model and systemPrompt are properly inherited from pathway configuration
21
+ *
22
+ * These tests simulate the core transformation logic from executePathwayWithFallback
23
+ * without requiring full integration test setup, allowing for focused unit testing
24
+ * of the argument transformation behavior.
25
+ */
26
+
27
+ import test from 'ava';
28
+
29
+ // Test the transformation logic directly without mocking
30
+ test('should format cortex pathway arguments correctly with existing chatHistory', (t) => {
31
+ // Mock the original prompt
32
+ const originalPrompt = {
33
+ name: 'summarize',
34
+ prompt: 'summarize this file',
35
+ cortexPathwayName: 'run_labeeb_agent'
36
+ };
37
+
38
+ // Mock pathway data
39
+ const pathway = {
40
+ model: 'labeeb-agent',
41
+ systemPrompt: 'Test system prompt'
42
+ };
43
+
44
+ // Mock incoming pathway args
45
+ const pathwayArgs = {
46
+ text: 'summarize the file',
47
+ chatHistory: [
48
+ {
49
+ role: 'user',
50
+ content: [
51
+ '{"type":"image_url","url":"test-url","image_url":{"url":"test-url"},"gcs":"test-gcs","originalFilename":"test.jpg","hash":"test-hash"}'
52
+ ]
53
+ }
54
+ ]
55
+ };
56
+
57
+ // Simulate the transformation logic from the executePathwayWithFallback function
58
+ const cortexArgs = {
59
+ model: pathway.model || pathwayArgs.model || "labeeb-agent",
60
+ chatHistory: [],
61
+ systemPrompt: pathway.systemPrompt
62
+ };
63
+
64
+ // If we have existing chatHistory, use it as base
65
+ if (pathwayArgs.chatHistory && pathwayArgs.chatHistory.length > 0) {
66
+ cortexArgs.chatHistory = JSON.parse(JSON.stringify(pathwayArgs.chatHistory));
67
+ }
68
+
69
+ // If we have text parameter, we need to add it to the chatHistory
70
+ if (pathwayArgs.text) {
71
+ // Find the last user message or create a new one
72
+ let lastUserMessage = null;
73
+ for (let i = cortexArgs.chatHistory.length - 1; i >= 0; i--) {
74
+ if (cortexArgs.chatHistory[i].role === 'user') {
75
+ lastUserMessage = cortexArgs.chatHistory[i];
76
+ break;
77
+ }
78
+ }
79
+
80
+ if (lastUserMessage) {
81
+ // Ensure content is an array
82
+ if (!Array.isArray(lastUserMessage.content)) {
83
+ lastUserMessage.content = [JSON.stringify({
84
+ type: "text",
85
+ text: lastUserMessage.content || ""
86
+ })];
87
+ }
88
+
89
+ // Add the text parameter as a text content item
90
+ const textFromPrompt = originalPrompt?.prompt || pathwayArgs.text;
91
+ lastUserMessage.content.unshift(JSON.stringify({
92
+ type: "text",
93
+ text: `${pathwayArgs.text}\n\n${textFromPrompt}`
94
+ }));
95
+ } else {
96
+ // Create new user message with text
97
+ const textFromPrompt = originalPrompt?.prompt || pathwayArgs.text;
98
+ cortexArgs.chatHistory.push({
99
+ role: 'user',
100
+ content: [JSON.stringify({
101
+ type: "text",
102
+ text: `${pathwayArgs.text}\n\n${textFromPrompt}`
103
+ })]
104
+ });
105
+ }
106
+ }
107
+
108
+ // Verify the transformation
109
+ t.is(cortexArgs.model, 'labeeb-agent');
110
+ t.is(cortexArgs.systemPrompt, 'Test system prompt');
111
+ t.is(cortexArgs.chatHistory.length, 1);
112
+
113
+ // Check that the user message has the correct structure
114
+ const userMessage = cortexArgs.chatHistory[0];
115
+ t.is(userMessage.role, 'user');
116
+ t.true(Array.isArray(userMessage.content));
117
+ t.is(userMessage.content.length, 2);
118
+
119
+ // Check the text content was added first
120
+ const textContent = JSON.parse(userMessage.content[0]);
121
+ t.is(textContent.type, 'text');
122
+ t.is(textContent.text, 'summarize the file\n\nsummarize this file');
123
+
124
+ // Check the image content is preserved second
125
+ const imageContent = JSON.parse(userMessage.content[1]);
126
+ t.is(imageContent.type, 'image_url');
127
+ t.is(imageContent.gcs, 'test-gcs');
128
+ });
129
+
130
+ test('should create new user message when no existing chatHistory', (t) => {
131
+ // Mock the original prompt
132
+ const originalPrompt = {
133
+ name: 'summarize',
134
+ prompt: 'summarize this file',
135
+ cortexPathwayName: 'run_labeeb_agent'
136
+ };
137
+
138
+ // Mock pathway data
139
+ const pathway = {
140
+ model: 'labeeb-agent',
141
+ systemPrompt: 'Test system prompt'
142
+ };
143
+
144
+ // Mock incoming pathway args with no chatHistory
145
+ const pathwayArgs = {
146
+ text: 'summarize the file'
147
+ };
148
+
149
+ // Simulate the transformation logic from the executePathwayWithFallback function
150
+ const cortexArgs = {
151
+ model: pathway.model || pathwayArgs.model || "labeeb-agent",
152
+ chatHistory: [],
153
+ systemPrompt: pathway.systemPrompt
154
+ };
155
+
156
+ // If we have existing chatHistory, use it as base
157
+ if (pathwayArgs.chatHistory && pathwayArgs.chatHistory.length > 0) {
158
+ cortexArgs.chatHistory = JSON.parse(JSON.stringify(pathwayArgs.chatHistory));
159
+ }
160
+
161
+ // If we have text parameter, we need to add it to the chatHistory
162
+ if (pathwayArgs.text) {
163
+ // Find the last user message or create a new one
164
+ let lastUserMessage = null;
165
+ for (let i = cortexArgs.chatHistory.length - 1; i >= 0; i--) {
166
+ if (cortexArgs.chatHistory[i].role === 'user') {
167
+ lastUserMessage = cortexArgs.chatHistory[i];
168
+ break;
169
+ }
170
+ }
171
+
172
+ if (lastUserMessage) {
173
+ // Ensure content is an array
174
+ if (!Array.isArray(lastUserMessage.content)) {
175
+ lastUserMessage.content = [JSON.stringify({
176
+ type: "text",
177
+ text: lastUserMessage.content || ""
178
+ })];
179
+ }
180
+
181
+ // Add the text parameter as a text content item
182
+ const textFromPrompt = originalPrompt?.prompt || pathwayArgs.text;
183
+ lastUserMessage.content.unshift(JSON.stringify({
184
+ type: "text",
185
+ text: `${pathwayArgs.text}\n\n${textFromPrompt}`
186
+ }));
187
+ } else {
188
+ // Create new user message with text
189
+ const textFromPrompt = originalPrompt?.prompt || pathwayArgs.text;
190
+ cortexArgs.chatHistory.push({
191
+ role: 'user',
192
+ content: [JSON.stringify({
193
+ type: "text",
194
+ text: `${pathwayArgs.text}\n\n${textFromPrompt}`
195
+ })]
196
+ });
197
+ }
198
+ }
199
+
200
+ // Verify the transformation
201
+ t.is(cortexArgs.model, 'labeeb-agent');
202
+ t.is(cortexArgs.systemPrompt, 'Test system prompt');
203
+ t.is(cortexArgs.chatHistory.length, 1);
204
+
205
+ // Check that a new user message was created
206
+ const userMessage = cortexArgs.chatHistory[0];
207
+ t.is(userMessage.role, 'user');
208
+ t.true(Array.isArray(userMessage.content));
209
+ t.is(userMessage.content.length, 1);
210
+
211
+ // Check the text content
212
+ const textContent = JSON.parse(userMessage.content[0]);
213
+ t.is(textContent.type, 'text');
214
+ t.is(textContent.text, 'summarize the file\n\nsummarize this file');
215
+ });
216
+
217
+ test('should use default model when pathway model is not specified', (t) => {
218
+ // Mock the original prompt
219
+ const originalPrompt = {
220
+ name: 'summarize',
221
+ prompt: 'summarize this file',
222
+ cortexPathwayName: 'run_labeeb_agent'
223
+ };
224
+
225
+ // Mock pathway data without model
226
+ const pathway = {
227
+ systemPrompt: 'Test system prompt'
228
+ };
229
+
230
+ // Mock incoming pathway args
231
+ const pathwayArgs = {
232
+ text: 'summarize the file'
233
+ };
234
+
235
+ // Simulate the transformation logic
236
+ const cortexArgs = {
237
+ model: pathway.model || pathwayArgs.model || "labeeb-agent",
238
+ chatHistory: [],
239
+ systemPrompt: pathway.systemPrompt
240
+ };
241
+
242
+ // Verify default model is used
243
+ t.is(cortexArgs.model, 'labeeb-agent');
244
+ });
@@ -2,6 +2,9 @@ import test from 'ava';
2
2
  import sinon from 'sinon';
3
3
  import { getResolvers } from '../../../server/graphql.js';
4
4
 
5
+ // Mock callPathway to avoid actual external calls
6
+ const mockCallPathway = sinon.stub();
7
+
5
8
  // Mock logger to avoid actual logging during tests
6
9
  const mockLogger = {
7
10
  info: sinon.stub(),
@@ -17,7 +20,7 @@ const mockConfig = {
17
20
 
18
21
  test.beforeEach(t => {
19
22
  // Reset stubs before each test
20
- sinon.restore();
23
+ mockCallPathway.resetHistory();
21
24
  mockLogger.info.resetHistory();
22
25
  mockLogger.debug.resetHistory();
23
26
  mockLogger.error.resetHistory();
@@ -168,3 +171,121 @@ test('executeWorkspace does not check format when promptNames not provided', asy
168
171
  t.false(mockPathwayManager.isLegacyPromptFormat.called);
169
172
  t.true(mockPathwayManager.getPathway.calledOnce);
170
173
  });
174
+
175
+ test('executeWorkspace helper function DRY refactoring - structure verification', async t => {
176
+ // This test verifies that the DRY refactoring doesn't break existing functionality
177
+ // by testing that all three code paths (wildcard, specific prompts, default)
178
+ // still work with fallback to legacy execution
179
+
180
+ const mockRootResolver = sinon.stub().resolves({ result: 'legacy-result' });
181
+
182
+ // Test wildcard case with legacy fallback
183
+ const mockPathwayManager = {
184
+ getLatestPathways: sinon.stub().resolves({
185
+ 'test-user': {
186
+ 'test-pathway': {
187
+ prompt: [
188
+ { name: 'prompt1' }, // No cortexPathwayName - will fallback
189
+ { name: 'prompt2' } // No cortexPathwayName - will fallback
190
+ ],
191
+ systemPrompt: 'Test system prompt'
192
+ }
193
+ }
194
+ }),
195
+ isLegacyPromptFormat: sinon.stub().returns(false),
196
+ getPathways: sinon.stub().resolves([
197
+ {
198
+ name: 'prompt1',
199
+ systemPrompt: 'System prompt 1',
200
+ prompt: [{ messages: ['message1'] }],
201
+ fileHashes: [],
202
+ rootResolver: mockRootResolver
203
+ },
204
+ {
205
+ name: 'prompt2',
206
+ systemPrompt: 'System prompt 2',
207
+ prompt: [{ messages: ['message2'] }],
208
+ fileHashes: [],
209
+ rootResolver: mockRootResolver
210
+ }
211
+ ]),
212
+ getResolvers: sinon.stub().returns({ Mutation: {} })
213
+ };
214
+
215
+ const resolvers = getResolvers(mockConfig, {}, mockPathwayManager);
216
+ const executeWorkspaceResolver = resolvers.Query.executeWorkspace;
217
+
218
+ const mockContextValue = { config: mockConfig };
219
+ const mockInfo = {};
220
+
221
+ const args = {
222
+ userId: 'test-user',
223
+ pathwayName: 'test-pathway',
224
+ promptNames: ['*'], // Wildcard to execute all
225
+ text: 'test input'
226
+ };
227
+
228
+ const result = await executeWorkspaceResolver(null, args, mockContextValue, mockInfo);
229
+
230
+ // Verify that legacy resolvers were called (indicating the DRY helper function worked)
231
+ t.is(mockRootResolver.callCount, 2); // Called twice for both prompts
232
+
233
+ // Verify result structure matches expected format
234
+ t.truthy(result);
235
+ t.truthy(result.result);
236
+ t.true(result.debug.includes('Executed 2 prompts in parallel'));
237
+
238
+ // Parse the result to verify both prompts were executed
239
+ const parsedResult = JSON.parse(result.result);
240
+ t.is(parsedResult.length, 2);
241
+ t.is(parsedResult[0].promptName, 'prompt1');
242
+ t.is(parsedResult[1].promptName, 'prompt2');
243
+ });
244
+
245
+ test('executeWorkspace helper function DRY refactoring - default case structure', async t => {
246
+ // Test that the default case still works with the DRY helper function
247
+
248
+ const mockRootResolver = sinon.stub().resolves({ result: 'default-legacy-result' });
249
+
250
+ const mockPathwayManager = {
251
+ getLatestPathways: sinon.stub().resolves({
252
+ 'test-user': {
253
+ 'test-pathway': {
254
+ prompt: [
255
+ { name: 'default-prompt' } // No cortexPathwayName
256
+ ],
257
+ systemPrompt: 'Test system prompt'
258
+ }
259
+ }
260
+ }),
261
+ getPathway: sinon.stub().resolves({
262
+ prompt: [{ name: 'default-prompt' }], // No cortexPathwayName
263
+ systemPrompt: 'Test system prompt',
264
+ fileHashes: [],
265
+ rootResolver: mockRootResolver
266
+ }),
267
+ getResolvers: sinon.stub().returns({ Mutation: {} })
268
+ };
269
+
270
+ const resolvers = getResolvers(mockConfig, {}, mockPathwayManager);
271
+ const executeWorkspaceResolver = resolvers.Query.executeWorkspace;
272
+
273
+ const mockContextValue = { config: mockConfig };
274
+ const mockInfo = {};
275
+
276
+ const args = {
277
+ userId: 'test-user',
278
+ pathwayName: 'test-pathway',
279
+ text: 'test input'
280
+ // No promptNames provided - uses default case
281
+ };
282
+
283
+ const result = await executeWorkspaceResolver(null, args, mockContextValue, mockInfo);
284
+
285
+ // Verify that legacy resolver was called (indicating DRY helper function worked for default case)
286
+ t.is(mockRootResolver.callCount, 1);
287
+
288
+ // Verify result structure
289
+ t.truthy(result);
290
+ t.is(result.result, 'default-legacy-result');
291
+ });