@abgov/nx-adsp 12.8.0-beta.2 → 12.8.0-beta.4
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/package.json +1 -1
- package/src/utils/agent.d.ts +5 -0
- package/src/utils/agent.js +214 -153
- package/src/utils/agent.js.map +1 -1
- package/src/utils/agent.spec.ts +18 -1
package/package.json
CHANGED
package/src/utils/agent.d.ts
CHANGED
|
@@ -25,6 +25,11 @@ export interface AgentResult {
|
|
|
25
25
|
* generated and modified files; this function retrieves the workspace state
|
|
26
26
|
* after the conversation and applies all files to the Nx Tree.
|
|
27
27
|
*
|
|
28
|
+
* The socket connection and file upload start immediately. While the files
|
|
29
|
+
* are uploading, the developer is prompted for a brief project description.
|
|
30
|
+
* The initial message is sent as soon as both the upload and the description
|
|
31
|
+
* are ready — whichever finishes last.
|
|
32
|
+
*
|
|
28
33
|
* Returns null if agent-service is unavailable — callers should skip the
|
|
29
34
|
* agent step gracefully in that case.
|
|
30
35
|
*/
|
package/src/utils/agent.js
CHANGED
|
@@ -13,11 +13,17 @@ const AGENT_ID = 'nxAdspAgent';
|
|
|
13
13
|
* generated and modified files; this function retrieves the workspace state
|
|
14
14
|
* after the conversation and applies all files to the Nx Tree.
|
|
15
15
|
*
|
|
16
|
+
* The socket connection and file upload start immediately. While the files
|
|
17
|
+
* are uploading, the developer is prompted for a brief project description.
|
|
18
|
+
* The initial message is sent as soon as both the upload and the description
|
|
19
|
+
* are ready — whichever finishes last.
|
|
20
|
+
*
|
|
16
21
|
* Returns null if agent-service is unavailable — callers should skip the
|
|
17
22
|
* agent step gracefully in that case.
|
|
18
23
|
*/
|
|
19
24
|
function consultAgent(directoryServiceUrl, accessToken, projectContext, host, projectRoot) {
|
|
20
25
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
26
|
+
var _a;
|
|
21
27
|
if (!accessToken) {
|
|
22
28
|
process.stdout.write('\n[nx-adsp] No access token — skipping agent interaction.\n');
|
|
23
29
|
return null;
|
|
@@ -28,52 +34,136 @@ function consultAgent(directoryServiceUrl, accessToken, projectContext, host, pr
|
|
|
28
34
|
return null;
|
|
29
35
|
}
|
|
30
36
|
process.stdout.write(`\n[nx-adsp] Connecting to agent at ${agentServiceUrl}...\n`);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
37
|
+
const threadId = crypto.randomUUID();
|
|
38
|
+
// Start socket connection immediately so file upload overlaps with the description prompt.
|
|
39
|
+
const socket = (0, socket_io_client_1.io)(agentServiceUrl, {
|
|
40
|
+
auth: { token: accessToken },
|
|
41
|
+
// Skip polling — go directly to WebSocket to avoid ARO ingress rejecting
|
|
42
|
+
// the polling POST with HTTP 400.
|
|
43
|
+
transports: ['websocket'],
|
|
44
|
+
timeout: 30000,
|
|
45
|
+
reconnection: false,
|
|
46
|
+
});
|
|
47
|
+
const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
48
|
+
// Coordination: sendInitialMessage is called once BOTH conditions are met:
|
|
49
|
+
// 1. description prompt has been answered (descriptionReady = true)
|
|
50
|
+
// 2. workspace files have been uploaded (workspaceReady = true)
|
|
51
|
+
// Whichever condition is satisfied last triggers the send.
|
|
52
|
+
let description;
|
|
53
|
+
let descriptionReady = false;
|
|
54
|
+
let workspaceReady = false;
|
|
55
|
+
let conversationStarted = false;
|
|
56
|
+
let buffer = '';
|
|
57
|
+
let conversationDone = false;
|
|
58
|
+
let agentHasResponded = false;
|
|
59
|
+
let interrupted = false;
|
|
60
|
+
let resolveConversation;
|
|
61
|
+
const conversationPromise = new Promise((r) => {
|
|
62
|
+
resolveConversation = r;
|
|
63
|
+
});
|
|
64
|
+
const cleanup = (filesWritten) => {
|
|
65
|
+
conversationDone = true;
|
|
66
|
+
rl.close();
|
|
67
|
+
socket.disconnect();
|
|
68
|
+
// Return null only for truly silent skips (no agent, no token, connection failed).
|
|
69
|
+
// Return a result whenever the user was actively engaged (agent responded)
|
|
70
|
+
// OR when they explicitly interrupted (Ctrl+C), so the caller always gets
|
|
71
|
+
// a chance to confirm before proceeding.
|
|
72
|
+
resolveConversation(agentHasResponded || interrupted
|
|
73
|
+
? { filesWritten, userInteracted: agentHasResponded, interrupted }
|
|
74
|
+
: null);
|
|
75
|
+
};
|
|
76
|
+
const buildInitialMessage = () => {
|
|
77
|
+
const fileNames = Object.keys(projectContext.existingFiles).join(', ');
|
|
78
|
+
const descriptionLine = description
|
|
79
|
+
? `It is described as: "${description}". `
|
|
80
|
+
: '';
|
|
81
|
+
return (`I am setting up a new ${projectContext.projectType} called "${projectContext.projectName}" ` +
|
|
82
|
+
`for ADSP tenant "${projectContext.tenant}" (nx-adsp plugin version ${projectContext.pluginVersion}). ` +
|
|
83
|
+
descriptionLine +
|
|
84
|
+
`The project files (${fileNames}) have been uploaded to your workspace. ` +
|
|
85
|
+
`What ADSP capabilities would be useful to integrate into this service?`);
|
|
86
|
+
};
|
|
87
|
+
const sendInitialMessage = () => {
|
|
88
|
+
if (conversationStarted)
|
|
89
|
+
return;
|
|
90
|
+
conversationStarted = true;
|
|
91
|
+
process.stdout.write('[nx-adsp] Type your replies at the > prompt. Press Ctrl+D or leave blank to apply generated files.\n\n');
|
|
92
|
+
socket.emit('message', {
|
|
93
|
+
agent: AGENT_ID,
|
|
94
|
+
threadId,
|
|
95
|
+
content: buildInitialMessage(),
|
|
96
|
+
rawChunks: true,
|
|
51
97
|
});
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
if (!conversationDone && !
|
|
98
|
+
// Show periodic dots while waiting for first agent response, then warn at 2 minutes.
|
|
99
|
+
const dotInterval = setInterval(() => {
|
|
100
|
+
if (!conversationDone && !agentHasResponded)
|
|
101
|
+
process.stdout.write('.');
|
|
102
|
+
else
|
|
103
|
+
clearInterval(dotInterval);
|
|
104
|
+
}, 3000);
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
clearInterval(dotInterval);
|
|
107
|
+
if (!conversationDone && !agentHasResponded) {
|
|
108
|
+
process.stdout.write('\n[nx-adsp] No response after 2 minutes. ' +
|
|
109
|
+
'The nxAdspAgent may still be deploying or the LLM is unresponsive. ' +
|
|
110
|
+
'Press Ctrl+C to skip and continue generation.\n');
|
|
111
|
+
}
|
|
112
|
+
}, 120000);
|
|
113
|
+
};
|
|
114
|
+
const requestWorkspaceState = () => {
|
|
115
|
+
socket.emit('workspace-read', { agent: AGENT_ID, threadId });
|
|
116
|
+
};
|
|
117
|
+
const promptUser = () => {
|
|
118
|
+
rl.question('\n> ', (input) => {
|
|
119
|
+
const trimmed = input.trim();
|
|
120
|
+
if (trimmed) {
|
|
121
|
+
socket.emit('message', {
|
|
122
|
+
agent: AGENT_ID,
|
|
123
|
+
threadId,
|
|
124
|
+
content: trimmed,
|
|
125
|
+
rawChunks: true,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// Empty input — apply whatever the agent has generated.
|
|
55
130
|
requestWorkspaceState();
|
|
56
131
|
}
|
|
57
132
|
});
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
133
|
+
};
|
|
134
|
+
const applyWorkspaceFiles = (files) => {
|
|
135
|
+
let count = 0;
|
|
136
|
+
for (const file of files) {
|
|
137
|
+
// Skip files we uploaded that the agent hasn't modified — only apply
|
|
138
|
+
// new files and agent-modified versions of existing files.
|
|
139
|
+
const original = projectContext.existingFiles[file.path];
|
|
140
|
+
if (original !== undefined && original === file.content) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const fullPath = `${projectRoot}/${file.path}`;
|
|
144
|
+
host.write(fullPath, file.content);
|
|
145
|
+
count++;
|
|
146
|
+
}
|
|
147
|
+
return count;
|
|
148
|
+
};
|
|
149
|
+
// Register all socket and readline handlers upfront so they are active
|
|
150
|
+
// while the description prompt is being shown.
|
|
151
|
+
rl.on('SIGINT', () => {
|
|
152
|
+
interrupted = true;
|
|
153
|
+
process.stdout.write('\n');
|
|
154
|
+
cleanup(0);
|
|
155
|
+
});
|
|
156
|
+
rl.on('close', () => {
|
|
157
|
+
if (!conversationDone && !interrupted) {
|
|
158
|
+
requestWorkspaceState();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
socket.on('connect', () => {
|
|
162
|
+
// Connect and upload silently — stdout writes here would interleave with
|
|
163
|
+
// the description prompt that is active concurrently.
|
|
164
|
+
// The server signals handler readiness via socket.send() after its async
|
|
165
|
+
// getServiceConfiguration() resolves; wait for that before uploading.
|
|
166
|
+
socket.once('message', () => {
|
|
77
167
|
socket.emit('workspace-update', {
|
|
78
168
|
agent: AGENT_ID,
|
|
79
169
|
threadId,
|
|
@@ -83,121 +173,92 @@ function consultAgent(directoryServiceUrl, accessToken, projectContext, host, pr
|
|
|
83
173
|
})),
|
|
84
174
|
deletes: [],
|
|
85
175
|
});
|
|
86
|
-
};
|
|
87
|
-
const promptUser = () => {
|
|
88
|
-
rl.question('\n> ', (input) => {
|
|
89
|
-
const trimmed = input.trim();
|
|
90
|
-
if (trimmed) {
|
|
91
|
-
sendMessage(trimmed);
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
// Empty input — apply whatever the agent has generated.
|
|
95
|
-
requestWorkspaceState();
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
};
|
|
99
|
-
const applyWorkspaceFiles = (files) => {
|
|
100
|
-
let count = 0;
|
|
101
|
-
for (const file of files) {
|
|
102
|
-
// Skip files we uploaded that the agent hasn't modified — only apply
|
|
103
|
-
// new files and agent-modified versions of existing files.
|
|
104
|
-
const original = projectContext.existingFiles[file.path];
|
|
105
|
-
if (original !== undefined && original === file.content) {
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
const fullPath = `${projectRoot}/${file.path}`;
|
|
109
|
-
host.write(fullPath, file.content);
|
|
110
|
-
count++;
|
|
111
|
-
}
|
|
112
|
-
return count;
|
|
113
|
-
};
|
|
114
|
-
const cleanup = (filesWritten) => {
|
|
115
|
-
conversationDone = true;
|
|
116
|
-
rl.close();
|
|
117
|
-
socket.disconnect();
|
|
118
|
-
// Return null only for truly silent skips (no agent, no token, connection failed).
|
|
119
|
-
// Return a result whenever the user was actively engaged (agent responded)
|
|
120
|
-
// OR when they explicitly interrupted (Ctrl+C), so the caller always gets
|
|
121
|
-
// a chance to confirm before proceeding.
|
|
122
|
-
resolve(agentHasResponded || interrupted
|
|
123
|
-
? { filesWritten, userInteracted: agentHasResponded, interrupted }
|
|
124
|
-
: null);
|
|
125
|
-
};
|
|
126
|
-
socket.on('workspace-updated', () => {
|
|
127
|
-
process.stdout.write('[nx-adsp] Project files uploaded to workspace.\n');
|
|
128
|
-
process.stdout.write('[nx-adsp] Type your replies at the > prompt. Press Ctrl+D or leave blank to apply generated files.\n\n');
|
|
129
|
-
sendMessage(buildInitialMessage());
|
|
130
|
-
});
|
|
131
|
-
socket.on('connect', () => {
|
|
132
|
-
process.stdout.write('[nx-adsp] Connected to agent-service.\n');
|
|
133
|
-
process.stdout.write('[nx-adsp] Uploading project files to workspace...\n');
|
|
134
|
-
uploadFilesToWorkspace();
|
|
135
|
-
// Show periodic dots while waiting for first response, then a warning at 2 minutes.
|
|
136
|
-
const dotInterval = setInterval(() => {
|
|
137
|
-
if (!conversationDone && !agentHasResponded)
|
|
138
|
-
process.stdout.write('.');
|
|
139
|
-
else
|
|
140
|
-
clearInterval(dotInterval);
|
|
141
|
-
}, 3000);
|
|
142
|
-
setTimeout(() => {
|
|
143
|
-
clearInterval(dotInterval);
|
|
144
|
-
if (!conversationDone && !agentHasResponded) {
|
|
145
|
-
process.stdout.write('\n[nx-adsp] No response after 2 minutes. ' +
|
|
146
|
-
'The nxAdspAgent may still be deploying or the LLM is unresponsive. ' +
|
|
147
|
-
'Press Ctrl+C to skip and continue generation.\n');
|
|
148
|
-
}
|
|
149
|
-
}, 120000);
|
|
150
176
|
});
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
buffer = '';
|
|
168
|
-
// Agent finished its turn — prompt the user to continue the conversation
|
|
169
|
-
// or press Enter to apply whatever the agent has written so far.
|
|
170
|
-
promptUser();
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
socket.on('workspace-state', ({ files }) => {
|
|
174
|
-
// Only apply files the agent added or modified — not unchanged uploaded files.
|
|
175
|
-
const written = applyWorkspaceFiles(files !== null && files !== void 0 ? files : []);
|
|
176
|
-
if (written > 0) {
|
|
177
|
-
process.stdout.write(`\nApplied ${written} file(s) from agent workspace.\n`);
|
|
177
|
+
});
|
|
178
|
+
socket.on('workspace-updated', () => {
|
|
179
|
+
workspaceReady = true;
|
|
180
|
+
if (descriptionReady) {
|
|
181
|
+
// Description was entered before upload finished — send now.
|
|
182
|
+
sendInitialMessage();
|
|
183
|
+
}
|
|
184
|
+
// else: description prompt is still open — post-prompt code will call sendInitialMessage.
|
|
185
|
+
});
|
|
186
|
+
socket.on('stream', ({ chunk, done }) => {
|
|
187
|
+
var _a, _b;
|
|
188
|
+
if ((chunk === null || chunk === void 0 ? void 0 : chunk.type) === 'text-delta') {
|
|
189
|
+
const text = (_b = (_a = chunk.payload) === null || _a === void 0 ? void 0 : _a.text) !== null && _b !== void 0 ? _b : '';
|
|
190
|
+
if (!agentHasResponded) {
|
|
191
|
+
// Clear the dots line before the first response starts.
|
|
192
|
+
process.stdout.write('\n');
|
|
178
193
|
}
|
|
179
|
-
|
|
180
|
-
|
|
194
|
+
buffer += text;
|
|
195
|
+
agentHasResponded = true;
|
|
196
|
+
process.stdout.write(text);
|
|
197
|
+
}
|
|
198
|
+
if (done) {
|
|
199
|
+
if (buffer.length > 0 && !buffer.endsWith('\n')) {
|
|
200
|
+
process.stdout.write('\n');
|
|
181
201
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
});
|
|
188
|
-
socket.on('connect_error', (err) => {
|
|
189
|
-
var _a, _b, _c, _d;
|
|
190
|
-
// Log the full error object so we can diagnose the root cause.
|
|
191
|
-
const errAny = err;
|
|
192
|
-
process.stdout.write(`\n[nx-adsp] Connection failed: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}\n`);
|
|
193
|
-
process.stdout.write(`[nx-adsp] Error detail: ${JSON.stringify((_d = (_c = (_b = errAny === null || errAny === void 0 ? void 0 : errAny.description) !== null && _b !== void 0 ? _b : errAny === null || errAny === void 0 ? void 0 : errAny.context) !== null && _c !== void 0 ? _c : errAny === null || errAny === void 0 ? void 0 : errAny.cause) !== null && _d !== void 0 ? _d : 'none')}\n`);
|
|
194
|
-
cleanup(0);
|
|
195
|
-
});
|
|
196
|
-
socket.on('error', (err) => {
|
|
197
|
-
process.stdout.write(`\n[nx-adsp] Agent error: ${JSON.stringify(err)}\n`);
|
|
198
|
-
requestWorkspaceState();
|
|
199
|
-
});
|
|
202
|
+
buffer = '';
|
|
203
|
+
// Agent finished its turn — prompt the user to continue the conversation
|
|
204
|
+
// or press Enter to apply whatever the agent has written so far.
|
|
205
|
+
promptUser();
|
|
206
|
+
}
|
|
200
207
|
});
|
|
208
|
+
socket.on('workspace-state', ({ files }) => {
|
|
209
|
+
// Only apply files the agent added or modified — not unchanged uploaded files.
|
|
210
|
+
const written = applyWorkspaceFiles(files !== null && files !== void 0 ? files : []);
|
|
211
|
+
if (written > 0) {
|
|
212
|
+
process.stdout.write(`\nApplied ${written} file(s) from agent workspace.\n`);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
process.stdout.write('\nNo files generated by agent.\n');
|
|
216
|
+
}
|
|
217
|
+
cleanup(written);
|
|
218
|
+
});
|
|
219
|
+
socket.on('session-expired', () => {
|
|
220
|
+
process.stdout.write('\nAgent session expired.\n');
|
|
221
|
+
requestWorkspaceState();
|
|
222
|
+
});
|
|
223
|
+
socket.on('connect_error', (err) => {
|
|
224
|
+
var _a, _b, _c, _d;
|
|
225
|
+
const errAny = err;
|
|
226
|
+
process.stdout.write(`\n[nx-adsp] Connection failed: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}\n`);
|
|
227
|
+
process.stdout.write(`[nx-adsp] Error detail: ${JSON.stringify((_d = (_c = (_b = errAny === null || errAny === void 0 ? void 0 : errAny.description) !== null && _b !== void 0 ? _b : errAny === null || errAny === void 0 ? void 0 : errAny.context) !== null && _c !== void 0 ? _c : errAny === null || errAny === void 0 ? void 0 : errAny.cause) !== null && _d !== void 0 ? _d : 'none')}\n`);
|
|
228
|
+
cleanup(0);
|
|
229
|
+
});
|
|
230
|
+
socket.on('error', (err) => {
|
|
231
|
+
process.stdout.write(`\n[nx-adsp] Agent error: ${JSON.stringify(err)}\n`);
|
|
232
|
+
requestWorkspaceState();
|
|
233
|
+
});
|
|
234
|
+
// Show description prompt while socket connects and uploads files in the background.
|
|
235
|
+
try {
|
|
236
|
+
const { prompt } = yield Promise.resolve().then(() => require('enquirer'));
|
|
237
|
+
const answer = yield prompt({
|
|
238
|
+
type: 'input',
|
|
239
|
+
name: 'description',
|
|
240
|
+
message: `Briefly describe what ${projectContext.projectName} does:`,
|
|
241
|
+
});
|
|
242
|
+
description = ((_a = answer.description) === null || _a === void 0 ? void 0 : _a.trim()) || undefined;
|
|
243
|
+
descriptionReady = true;
|
|
244
|
+
}
|
|
245
|
+
catch (_b) {
|
|
246
|
+
// Ctrl+C or cancellation during the description prompt.
|
|
247
|
+
interrupted = true;
|
|
248
|
+
cleanup(0);
|
|
249
|
+
return conversationPromise;
|
|
250
|
+
}
|
|
251
|
+
if (workspaceReady) {
|
|
252
|
+
// Upload finished while the user was typing — send immediately.
|
|
253
|
+
sendInitialMessage();
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
// Rare: user typed faster than the upload completed. Show a brief status
|
|
257
|
+
// so the prompt doesn't appear to hang.
|
|
258
|
+
process.stdout.write('[nx-adsp] Uploading project files to workspace...\n');
|
|
259
|
+
// workspace-updated handler will call sendInitialMessage once upload completes.
|
|
260
|
+
}
|
|
261
|
+
return conversationPromise;
|
|
201
262
|
});
|
|
202
263
|
}
|
|
203
264
|
function resolveAgentServiceUrl(directoryServiceUrl) {
|
package/src/utils/agent.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../../../packages/nx-adsp/src/utils/agent.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../../../packages/nx-adsp/src/utils/agent.ts"],"names":[],"mappings":"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"}
|
package/src/utils/agent.spec.ts
CHANGED
|
@@ -12,6 +12,12 @@ jest.mock('socket.io-client');
|
|
|
12
12
|
jest.mock('@abgov/nx-oc', () => ({ getServiceUrls: jest.fn() }));
|
|
13
13
|
jest.mock('@nx/devkit', () => ({ Tree: jest.fn() }));
|
|
14
14
|
|
|
15
|
+
// Enquirer is dynamically imported inside consultAgent; mock it here so the
|
|
16
|
+
// prompt resolves immediately with an empty description in all tests.
|
|
17
|
+
jest.mock('enquirer', () => ({
|
|
18
|
+
prompt: jest.fn().mockResolvedValue({ description: '' }),
|
|
19
|
+
}));
|
|
20
|
+
|
|
15
21
|
import { io } from 'socket.io-client';
|
|
16
22
|
import { getServiceUrls } from '@abgov/nx-oc';
|
|
17
23
|
|
|
@@ -37,6 +43,9 @@ function makeMockSocket() {
|
|
|
37
43
|
on: jest.fn((event: string, handler: (...args: unknown[]) => void) => {
|
|
38
44
|
handlers[event] = handler;
|
|
39
45
|
}),
|
|
46
|
+
once: jest.fn((event: string, handler: (...args: unknown[]) => void) => {
|
|
47
|
+
handlers[event] = handler;
|
|
48
|
+
}),
|
|
40
49
|
emit: jest.fn(),
|
|
41
50
|
disconnect: jest.fn(),
|
|
42
51
|
_handlers: handlers,
|
|
@@ -46,6 +55,9 @@ function makeMockSocket() {
|
|
|
46
55
|
return socket;
|
|
47
56
|
}
|
|
48
57
|
|
|
58
|
+
// Flush the microtask queue so async operations inside consultAgent settle.
|
|
59
|
+
// One tick is enough: the dynamic import and the enquirer mock both resolve
|
|
60
|
+
// as microtasks, so after this all handlers are registered and descriptionReady=true.
|
|
49
61
|
const flushPromises = () => new Promise(resolve => setTimeout(resolve, 0));
|
|
50
62
|
|
|
51
63
|
describe('consultAgent', () => {
|
|
@@ -66,6 +78,7 @@ describe('consultAgent', () => {
|
|
|
66
78
|
});
|
|
67
79
|
const socket = makeMockSocket();
|
|
68
80
|
const resultPromise = consultAgent('https://directory.example.com', 'token', PROJECT_CONTEXT, mockHost, 'apps/test-service');
|
|
81
|
+
// After one tick: enquirer mock resolves, descriptionReady=true, conversationPromise returned.
|
|
69
82
|
await flushPromises();
|
|
70
83
|
socket._handlers['connect_error']?.();
|
|
71
84
|
expect(await resultPromise).toBeNull();
|
|
@@ -79,7 +92,10 @@ describe('consultAgent', () => {
|
|
|
79
92
|
const resultPromise = consultAgent('https://directory.example.com', 'token', PROJECT_CONTEXT, mockHost, 'apps/test-service');
|
|
80
93
|
await flushPromises();
|
|
81
94
|
|
|
95
|
+
// descriptionReady=true at this point; workspace-updated fires last → triggers sendInitialMessage.
|
|
82
96
|
socket._handlers['connect']?.();
|
|
97
|
+
// Server signals readiness via 'message' before workspace-update is safe to send.
|
|
98
|
+
socket._handlers['message']?.('Connected as user...');
|
|
83
99
|
socket._handlers['workspace-updated']?.();
|
|
84
100
|
// Simulate agent responding with text (sets agentHasResponded = true)
|
|
85
101
|
socket._handlers['stream']?.({ chunk: { type: 'text-delta', payload: { text: 'I will add events.' } }, done: false });
|
|
@@ -105,8 +121,9 @@ describe('consultAgent', () => {
|
|
|
105
121
|
const resultPromise = consultAgent('https://directory.example.com', 'token', PROJECT_CONTEXT, mockHost, 'apps/test-service');
|
|
106
122
|
await flushPromises();
|
|
107
123
|
|
|
108
|
-
// connect → workspace-update emitted
|
|
124
|
+
// connect → server sends 'message' readiness signal → workspace-update emitted → workspace-updated → initial message sent
|
|
109
125
|
socket._handlers['connect']?.();
|
|
126
|
+
socket._handlers['message']?.('Connected as user...');
|
|
110
127
|
socket._handlers['workspace-updated']?.();
|
|
111
128
|
socket._handlers['stream']?.({ chunk: { type: 'text-delta', payload: { text: 'What does this service do?' } }, done: false });
|
|
112
129
|
socket._handlers['stream']?.({ chunk: null, done: true });
|