@slorenzot/mcp-azure 2.6.0 → 2.6.1
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/PRD_CONCURRENCIA_ADD_ATTACHMENT.md +512 -0
- package/RELEASE_BUG_FIXES.md +164 -0
- package/RELEASE_HISTORICAL.md +85 -0
- package/RELEASE_V2_4_0.md +63 -0
- package/RELEASE_V2_6_0.md +423 -0
- package/RELEASE_V2_6_1.md +350 -0
- package/dist/index.js +148 -64
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
# PRD: Corrección de Concurrencia en add_attachment
|
|
2
|
+
|
|
3
|
+
## 📋 Resumen Ejecutivo
|
|
4
|
+
|
|
5
|
+
Corregir el problema de concurrencia en `ado_add_attachment` que causa el error **TF26071** cuando múltiples intentos de agregar adjuntos ocurren simultáneamente al mismo Work Item en Azure DevOps.
|
|
6
|
+
|
|
7
|
+
## 🐛 Problema
|
|
8
|
+
|
|
9
|
+
### Error Reportado
|
|
10
|
+
```
|
|
11
|
+
TF26071: This work item has been changed by someone else since you opened it.
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### Causa Raíz
|
|
15
|
+
Cuando múltiples llamadas a `ado_add_attachment` golpean el mismo `workItemId` en paralelo, Azure DevOps rechaza los cambios debido a conflicto de revisión. Cada `add_attachment` incrementa la revisión del Work Item.
|
|
16
|
+
|
|
17
|
+
### Flujo del Problema
|
|
18
|
+
1. **Usuario A** ejecuta `add_attachment` para WI #123 → Recupera WI versión 10
|
|
19
|
+
2. **Usuario B** ejecuta `add_attachment` para WI #123 → Recupera WI versión 10
|
|
20
|
+
3. **Usuario A** intenta actualizar WI #123 con adjunto → Azure DevOps acepta (versión 10→11)
|
|
21
|
+
4. **Usuario B** intenta actualizar WI #123 con adjunto → **TF26071**: Versión 10 ya no es la actual
|
|
22
|
+
5. **Resultado**: El segundo adjunto falla con error confuso para el usuario
|
|
23
|
+
|
|
24
|
+
### Impacto
|
|
25
|
+
- ❌ **Pérdida de funcionalidad**: Los segundos intentos fallan
|
|
26
|
+
- ❌ **Experiencia de usuario**: Errores técnicos no comprensibles
|
|
27
|
+
- ❌ **Inconsistencia**: A veces funciona, a veces falla (race condition)
|
|
28
|
+
- ❌ **Sin workaround documentado**: Los usuarios no saben cómo manejarlo
|
|
29
|
+
|
|
30
|
+
## ✅ Solución Propuesta
|
|
31
|
+
|
|
32
|
+
### Estrategia Principal: Sistema de Cola por Work Item
|
|
33
|
+
|
|
34
|
+
Implementar una cola interna por Work Item que asegure que las operaciones de adjuntos sean **secuenciales** por WI, permitiendo paralelismo entre diferentes Work Items.
|
|
35
|
+
|
|
36
|
+
### Arquitectura de la Solución
|
|
37
|
+
|
|
38
|
+
#### 1. Sistema de Cola por Work Item
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
interface AttachmentQueue {
|
|
42
|
+
// Mapa de colas por Work Item ID
|
|
43
|
+
queues: Map<number, Promise<AttachmentResult>>;
|
|
44
|
+
|
|
45
|
+
// Agregar operación a la cola del WI
|
|
46
|
+
addToQueue(workItemId: number, operation: () => Promise<AttachmentResult>): Promise<AttachmentResult>;
|
|
47
|
+
|
|
48
|
+
// Verificar si hay cola activa para el WI
|
|
49
|
+
hasActiveQueue(workItemId: number): boolean;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### 2. Retry Automático con Re-fetch
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
async function addAttachmentWithRetry(
|
|
57
|
+
workItemId: number,
|
|
58
|
+
attachmentData: AttachmentData,
|
|
59
|
+
maxRetries: number = 3
|
|
60
|
+
): Promise<AttachmentResult> {
|
|
61
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
62
|
+
try {
|
|
63
|
+
// 1. Recuperar versión actual del WI (importante para cada intento)
|
|
64
|
+
const currentWI = await getWorkItem(workItemId);
|
|
65
|
+
|
|
66
|
+
// 2. Agregar adjunto usando la versión actual
|
|
67
|
+
return await addAttachment(workItemId, attachmentData);
|
|
68
|
+
|
|
69
|
+
} catch (error: any) {
|
|
70
|
+
// 3. Verificar si es error de concurrencia TF26071
|
|
71
|
+
if (isConcurrencyError(error) && attempt < maxRetries) {
|
|
72
|
+
// 4. Esperar backoff exponencial
|
|
73
|
+
await sleep(Math.pow(2, attempt) * 1000); // 2s, 4s, 8s
|
|
74
|
+
|
|
75
|
+
// 5. Re-fetch del WI para obtener la nueva versión
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 6. Si no es TF26071 o se agotaron reintentos, lanzar error
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### 3. Integración en `ado_add_attachment`
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
server.tool(
|
|
90
|
+
"ado_add_attachment",
|
|
91
|
+
"Agrega un adjunto a un Work Item existente con manejo de concurrencia",
|
|
92
|
+
// ... parámetros existentes ...
|
|
93
|
+
async ({ workItemId, filePath, attachmentUrl, comment, name }) => {
|
|
94
|
+
validateConnection();
|
|
95
|
+
validateProject();
|
|
96
|
+
|
|
97
|
+
// Verificar si hay cola activa para este WI
|
|
98
|
+
if (attachmentQueue.hasActiveQueue(workItemId)) {
|
|
99
|
+
console.error(`⏳ Encolando adjunto para WI #${workItemId} (ya hay operación en curso)`);
|
|
100
|
+
return await attachmentQueue.addToQueue(workItemId, () =>
|
|
101
|
+
addAttachmentWithRetry(workItemId, { filePath, attachmentUrl, comment, name })
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// No hay cola activa, ejecutar directamente con retry
|
|
106
|
+
console.error(`🚀 Ejecutando adjunto para WI #${workItemId}`);
|
|
107
|
+
return await addAttachmentWithRetry(workItemId, { filePath, attachmentUrl, comment, name });
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## 🔧 Implementación Detallada
|
|
113
|
+
|
|
114
|
+
### Paso 1: Funciones de Utilidad
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// Detectar si el error es TF26071 (concurrencia)
|
|
118
|
+
function isConcurrencyError(error: any): boolean {
|
|
119
|
+
const errorString = error?.message || error?.toString() || '';
|
|
120
|
+
return errorString.includes('TF26071') ||
|
|
121
|
+
errorString.includes('changed by someone else');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Función de sleep para backoff
|
|
125
|
+
function sleep(ms: number): Promise<void> {
|
|
126
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Obtener Work Item actualizado (para re-fetch en retry)
|
|
130
|
+
async function getWorkItem(workItemId: number): Promise<witInterfaces.WorkItem> {
|
|
131
|
+
const api = await getWitApi();
|
|
132
|
+
return await safeApiCall(
|
|
133
|
+
() => api.getWorkItem(workItemId, undefined, undefined, witInterfaces.WorkItemExpand.All),
|
|
134
|
+
`Error al obtener Work Item #${workItemId}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Paso 2: Implementación de Cola
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// Sistema de cola por Work Item
|
|
143
|
+
class AttachmentQueueManager {
|
|
144
|
+
private queues: Map<number, Promise<any>> = new Map();
|
|
145
|
+
|
|
146
|
+
async addToQueue<T>(
|
|
147
|
+
workItemId: number,
|
|
148
|
+
operation: () => Promise<T>
|
|
149
|
+
): Promise<T> {
|
|
150
|
+
// Si ya hay cola para este WI, esperar
|
|
151
|
+
const existingQueue = this.queues.get(workItemId);
|
|
152
|
+
if (existingQueue) {
|
|
153
|
+
console.error(`⏳ Esperando cola para WI #${workItemId}...`);
|
|
154
|
+
try {
|
|
155
|
+
await existingQueue;
|
|
156
|
+
} catch {
|
|
157
|
+
// Si la operación anterior falló, proceder con la nuestra
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Crear nueva cola para este WI
|
|
162
|
+
const queue = operation().finally(() => {
|
|
163
|
+
// Limpiar cola cuando termine
|
|
164
|
+
this.queues.delete(workItemId);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
this.queues.set(workItemId, queue);
|
|
168
|
+
return queue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
hasActiveQueue(workItemId: number): boolean {
|
|
172
|
+
return this.queues.has(workItemId);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Instancia global del gestor de colas
|
|
177
|
+
const attachmentQueue = new AttachmentQueueManager();
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Paso 3: Función Principal con Retry
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// Función principal con retry y re-fetch
|
|
184
|
+
async function addAttachmentWithRetry(
|
|
185
|
+
api: witApi.IWorkItemTrackingApi,
|
|
186
|
+
workItemId: number,
|
|
187
|
+
attachmentData: {
|
|
188
|
+
filePath?: string;
|
|
189
|
+
attachmentUrl?: string;
|
|
190
|
+
comment?: string;
|
|
191
|
+
name?: string;
|
|
192
|
+
},
|
|
193
|
+
maxRetries: number = 3
|
|
194
|
+
): Promise<{ content: any }> {
|
|
195
|
+
let lastError: any = null;
|
|
196
|
+
|
|
197
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
198
|
+
try {
|
|
199
|
+
console.error(`🔄 Intento ${attempt}/${maxRetries} para adjuntar a WI #${workItemId}`);
|
|
200
|
+
|
|
201
|
+
// 1. Recuperar versión actual del WI (CRÍTICO para cada intento)
|
|
202
|
+
const currentWI = await getWorkItem(workItemId);
|
|
203
|
+
console.error(`📄 WI #${workItemId} versión actual: ${currentWI.rev}`);
|
|
204
|
+
|
|
205
|
+
// 2. Procesar adjunto
|
|
206
|
+
let attachmentId: string | undefined;
|
|
207
|
+
let fileName: string | undefined;
|
|
208
|
+
let attachmentLinkUrl: string | undefined;
|
|
209
|
+
|
|
210
|
+
if (attachmentData.filePath) {
|
|
211
|
+
// Subir archivo nuevo
|
|
212
|
+
fileName = attachmentData.name || path.basename(attachmentData.filePath);
|
|
213
|
+
const attachment = await uploadAttachmentRest(attachmentData.filePath, fileName);
|
|
214
|
+
attachmentId = attachment.id;
|
|
215
|
+
attachmentLinkUrl = attachment.url;
|
|
216
|
+
} else if (attachmentData.attachmentUrl) {
|
|
217
|
+
// Usar adjunto existente
|
|
218
|
+
attachmentId = attachmentData.attachmentUrl.split('/attachments/')[1]?.split('?')[0];
|
|
219
|
+
fileName = attachmentData.name || "Archivo adjunto";
|
|
220
|
+
const baseUrl = currentOrg.endsWith("/") ? currentOrg.slice(0, -1) : currentOrg;
|
|
221
|
+
const encodedProject = getEncodedProject(currentProject);
|
|
222
|
+
attachmentLinkUrl = `${baseUrl}/${encodedProject}/_apis/wit/attachments/${attachmentId}`;
|
|
223
|
+
} else {
|
|
224
|
+
throw new Error("Debe proporcionar filePath o attachmentUrl");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 3. Vincular adjunto al WI usando la versión ACTUAL
|
|
228
|
+
const patchDocument: VSSInterfaces.JsonPatchOperation[] = [
|
|
229
|
+
{
|
|
230
|
+
op: VSSInterfaces.Operation.Add,
|
|
231
|
+
path: "/relations/-",
|
|
232
|
+
value: {
|
|
233
|
+
rel: "AttachedFile",
|
|
234
|
+
url: attachmentLinkUrl,
|
|
235
|
+
attributes: {
|
|
236
|
+
name: fileName,
|
|
237
|
+
comment: attachmentData.comment || "",
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
// 4. Actualizar WI con la versión actual
|
|
244
|
+
const updatedWI = await api.updateWorkItem(
|
|
245
|
+
null, // project
|
|
246
|
+
patchDocument,
|
|
247
|
+
workItemId
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
console.error(`✅ Adjunto agregado exitosamente a WI #${workItemId}`);
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
content: [{
|
|
254
|
+
type: "text",
|
|
255
|
+
text: `Adjunto agregado exitosamente al Work Item #${workItemId}\n- Nombre: ${fileName}\n- URL: ${attachmentLinkUrl}`,
|
|
256
|
+
}],
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
} catch (error: any) {
|
|
260
|
+
lastError = error;
|
|
261
|
+
console.error(`❌ Error en intento ${attempt}:`, error.message);
|
|
262
|
+
|
|
263
|
+
// Verificar si es error de concurrencia y quedan reintentos
|
|
264
|
+
if (isConcurrencyError(error) && attempt < maxRetries) {
|
|
265
|
+
const backoffTime = Math.pow(2, attempt) * 1000;
|
|
266
|
+
console.error(`⏳ Error TF26071 detectado. Esperando ${backoffTime}ms antes de reintentar...`);
|
|
267
|
+
await sleep(backoffTime);
|
|
268
|
+
console.error(`🔄 Reintentando con nueva versión del WI...`);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Si no es TF26071 o se agotaron reintentos, lanzar error
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Si se agotaron todos los reintentos, lanzar el último error
|
|
278
|
+
throw lastError;
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Paso 4: Actualización de `ado_add_attachment`
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
server.tool(
|
|
286
|
+
"ado_add_attachment",
|
|
287
|
+
"Agrega un adjunto a un Work Item existente. Maneja automáticamente concurrencia cuando múltiples adjuntos se agregan al mismo WI.",
|
|
288
|
+
{
|
|
289
|
+
workItemId: z.number().describe("ID del Work Item"),
|
|
290
|
+
filePath: z.string().optional().describe("Ruta del archivo a subir (opcional si se usa attachmentUrl)"),
|
|
291
|
+
attachmentUrl: z.string().optional().describe("URL de un adjunto ya subido (opcional si se usa filePath)"),
|
|
292
|
+
comment: z.string().optional().describe("Comentario para el adjunto"),
|
|
293
|
+
name: z.string().optional().describe("Nombre del archivo (si no se especifica, usa el nombre del archivo original)"),
|
|
294
|
+
},
|
|
295
|
+
async ({ workItemId, filePath, attachmentUrl, comment, name }) => {
|
|
296
|
+
validateConnection();
|
|
297
|
+
validateProject();
|
|
298
|
+
|
|
299
|
+
const api = await getWitApi();
|
|
300
|
+
|
|
301
|
+
// VERIFICAR CONCURRENCIA: Usar sistema de cola
|
|
302
|
+
if (attachmentQueue.hasActiveQueue(workItemId)) {
|
|
303
|
+
console.error(`⏳ Encolando adjunto para WI #${workItemId} (ya hay operación en curso)`);
|
|
304
|
+
return await attachmentQueue.addToQueue(workItemId, () =>
|
|
305
|
+
addAttachmentWithRetry(api, workItemId, { filePath, attachmentUrl, comment, name })
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Sin concurrencia: ejecutar directamente
|
|
310
|
+
console.error(`🚀 Ejecutando adjunto para WI #${workItemId}`);
|
|
311
|
+
return await attachmentQueue.addToQueue(workItemId, () =>
|
|
312
|
+
addAttachmentWithRetry(api, workItemId, { filePath, attachmentUrl, comment, name })
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## 📊 Casos de Uso
|
|
319
|
+
|
|
320
|
+
### Caso 1: Adjunto Único (sin concurrencia)
|
|
321
|
+
```
|
|
322
|
+
Usuario: add_attachment(123, "archivo.pdf")
|
|
323
|
+
Resultado: ✅ Adjunto agregado en primer intento
|
|
324
|
+
Logs:
|
|
325
|
+
🚀 Ejecutando adjunto para WI #123
|
|
326
|
+
📄 WI #123 versión actual: 10
|
|
327
|
+
✅ Adjunto agregado exitosamente a WI #123
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Caso 2: Concurrencia (2 adjuntos simultáneos)
|
|
331
|
+
```
|
|
332
|
+
Usuario A (hilo 1): add_attachment(123, "archivo1.pdf")
|
|
333
|
+
Usuario B (hilo 2): add_attachment(123, "archivo2.pdf")
|
|
334
|
+
|
|
335
|
+
Resultado:
|
|
336
|
+
Hilo 1: ✅ Adjunto agregado en primer intento
|
|
337
|
+
Hilo 2: ⏳ Encolado, espera a que termine hilo 1
|
|
338
|
+
Hilo 2: ✅ Adjunto agregado después de que hilo 1 terminó
|
|
339
|
+
|
|
340
|
+
Logs:
|
|
341
|
+
Hilo 1: 🚀 Ejecutando adjunto para WI #123
|
|
342
|
+
Hilo 2: ⏳ Encolando adjunto para WI #123 (ya hay operación en curso)
|
|
343
|
+
Hilo 1: 📄 WI #123 versión actual: 10
|
|
344
|
+
Hilo 1: ✅ Adjunto agregado exitosamente a WI #123
|
|
345
|
+
Hilo 2: 📄 WI #123 versión actual: 11
|
|
346
|
+
Hilo 2: ✅ Adjunto agregado exitosamente a WI #123
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Caso 3: Concurrencia con TF26071 (3 adjuntos simultáneos)
|
|
350
|
+
```
|
|
351
|
+
Usuario A, B, C: add_attachment(123, "archivoX.pdf")
|
|
352
|
+
|
|
353
|
+
Resultado:
|
|
354
|
+
Hilo 1: ✅ Adjunto agregado (versión 10→11)
|
|
355
|
+
Hilo 2: ⏳ Encolado, espera hilo 1
|
|
356
|
+
Hilo 3: ⏳ Encolado, espera hilo 1
|
|
357
|
+
Hilo 2: 🔄 Intento 1 → TF26071 → Reintenta
|
|
358
|
+
Hilo 2: 📄 WI #123 versión actual: 11
|
|
359
|
+
Hilo 2: ✅ Adjunto agregado (versión 11→12)
|
|
360
|
+
Hilo 3: 🔄 Intento 1 → TF26071 → Reintenta
|
|
361
|
+
Hilo 3: 📄 WI #123 versión actual: 12
|
|
362
|
+
Hilo 3: ✅ Adjunto agregado (versión 12→13)
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## ✅ Beneficios
|
|
366
|
+
|
|
367
|
+
### Para Usuarios
|
|
368
|
+
- ✅ **Sin errores TF26071**: Manejo automático de concurrencia
|
|
369
|
+
- ✅ **Transparencia**: El sistema maneja los reintentos automáticamente
|
|
370
|
+
- ✅ **Predictibilidad**: Los adjuntos siempre se agregan, eventualmente
|
|
371
|
+
- ✅ **Mejor experiencia**: Sin necesidad de reintentar manualmente
|
|
372
|
+
|
|
373
|
+
### Para Desarrolladores
|
|
374
|
+
- ✅ **Código limpio**: Lógica de concurrencia encapsulada
|
|
375
|
+
- ✅ **Reutilizable**: Sistema de cola puede usarse para otras operaciones
|
|
376
|
+
- ✅ **Debuggable**: Logs claros de lo que está pasando
|
|
377
|
+
- ✅ **Testable**: Lógica separada en funciones pequeñas
|
|
378
|
+
|
|
379
|
+
## 🧪 Testing
|
|
380
|
+
|
|
381
|
+
### Test Unitarios
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// Test: Detección de error TF26071
|
|
385
|
+
describe('isConcurrencyError', () => {
|
|
386
|
+
it('debería detectar error TF26071', () => {
|
|
387
|
+
const error = new Error('TF26071: This work item has been changed by someone else since you opened it.');
|
|
388
|
+
expect(isConcurrencyError(error)).toBe(true);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('no debería detectar otros errores', () => {
|
|
392
|
+
const error = new Error('Network error');
|
|
393
|
+
expect(isConcurrencyError(error)).toBe(false);
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Test: Sistema de cola
|
|
398
|
+
describe('AttachmentQueueManager', () => {
|
|
399
|
+
it('debería encolar operaciones concurrentes al mismo WI', async () => {
|
|
400
|
+
const queue = new AttachmentQueueManager();
|
|
401
|
+
const results: string[] = [];
|
|
402
|
+
|
|
403
|
+
// Simular 3 operaciones concurrentes al mismo WI
|
|
404
|
+
const op1 = queue.addToQueue(123, async () => { await sleep(100); return 'A'; });
|
|
405
|
+
const op2 = queue.addToQueue(123, async () => { await sleep(50); return 'B'; });
|
|
406
|
+
const op3 = queue.addToQueue(123, async () => { await sleep(50); return 'C'; });
|
|
407
|
+
|
|
408
|
+
results.push(await op1, await op2, await op3);
|
|
409
|
+
expect(results).toEqual(['A', 'B', 'C']);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Test de Integración
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
// Test: Retry automático con concurrencia simulada
|
|
418
|
+
describe('addAttachmentWithRetry', () => {
|
|
419
|
+
it('debería reintentar automáticamente al recibir TF26071', async () => {
|
|
420
|
+
let callCount = 0;
|
|
421
|
+
|
|
422
|
+
// Mock de API que falla 2 veces con TF26071, luego tiene éxito
|
|
423
|
+
const mockApi = {
|
|
424
|
+
updateWorkItem: jest.fn().mockImplementation(async () => {
|
|
425
|
+
callCount++;
|
|
426
|
+
if (callCount <= 2) {
|
|
427
|
+
throw new Error('TF26071: This work item has been changed by someone else since you opened it.');
|
|
428
|
+
}
|
|
429
|
+
return { id: 123, rev: 10 + callCount };
|
|
430
|
+
})
|
|
431
|
+
} as any;
|
|
432
|
+
|
|
433
|
+
const result = await addAttachmentWithRetry(mockApi, 123, { filePath: 'test.pdf' });
|
|
434
|
+
|
|
435
|
+
expect(mockApi.updateWorkItem).toHaveBeenCalledTimes(3);
|
|
436
|
+
expect(callCount).toBe(3);
|
|
437
|
+
expect(result).toBeDefined();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
## 🚀 Plan de Implementación
|
|
443
|
+
|
|
444
|
+
### Fase 1: Implementación Core (Sprint 1)
|
|
445
|
+
- [ ] Funciones de utilidad: `isConcurrencyError`, `sleep`, `getWorkItem`
|
|
446
|
+
- [ ] Clase `AttachmentQueueManager`
|
|
447
|
+
- [ ] Función `addAttachmentWithRetry` con lógica de retry
|
|
448
|
+
- [ ] Tests unitarios para nuevas funciones
|
|
449
|
+
|
|
450
|
+
### Fase 2: Integración (Sprint 1)
|
|
451
|
+
- [ ] Actualizar `ado_add_attachment` para usar sistema de cola
|
|
452
|
+
- [ ] Agregar logs informativos de concurrencia
|
|
453
|
+
- [ ] Tests de integración end-to-end
|
|
454
|
+
|
|
455
|
+
### Fase 3: Documentación (Sprint 1)
|
|
456
|
+
- [ ] Actualizar README.md con manejo de concurrencia
|
|
457
|
+
- [ ] Agregar ejemplos de uso con concurrencia
|
|
458
|
+
- [ ] Documentar comportamientos esperados
|
|
459
|
+
|
|
460
|
+
## 📝 Notas Técnicas
|
|
461
|
+
|
|
462
|
+
### Consideraciones de Performance
|
|
463
|
+
- **Cola por WI**: Solo serializa operaciones del mismo WI, permite paralelismo entre diferentes WIs
|
|
464
|
+
- **Backoff exponencial**: Evita sobrecargar Azure DevOps con reintentos agresivos
|
|
465
|
+
- **Re-fetch eficiente**: Solo recupera datos necesarios del WI, no campos completos
|
|
466
|
+
|
|
467
|
+
### Compatibilidad hacia atrás
|
|
468
|
+
- ✅ **Sin breaking changes**: Comportamiento automático y transparente
|
|
469
|
+
- ✅ **Opcional**: Si no hay concurrencia, funciona igual que antes
|
|
470
|
+
- ✅ **Configurable**: `maxRetries` puede ajustarse por usuario o config
|
|
471
|
+
|
|
472
|
+
### Manejo de Edge Cases
|
|
473
|
+
- ✅ **Timeout de colas**: Las colas no deben quedarse esperando para siempre
|
|
474
|
+
- ✅ **Error en operación anterior**: Si la op anterior falló, la siguiente debe proceder
|
|
475
|
+
- ✅ **Limpieza de colas**: Las colas se limpian automáticamente cuando terminan
|
|
476
|
+
|
|
477
|
+
## 🎯 Criterios de Éxito
|
|
478
|
+
|
|
479
|
+
### Funcionales
|
|
480
|
+
- ✅ Los adjuntos se agregan exitosamente incluso con concurrencia
|
|
481
|
+
- ✅ No se observan más errores TF26071 en producción
|
|
482
|
+
- ✅ El sistema maneja automáticamente los reintentos
|
|
483
|
+
- ✅ Las operaciones a diferentes WIs se ejecutan en paralelo
|
|
484
|
+
|
|
485
|
+
### No Funcionales
|
|
486
|
+
- ✅ Latencia aceptable (< 5s adicional con retry)
|
|
487
|
+
- ✅ Sin memory leaks en sistema de cola
|
|
488
|
+
- ✅ Logs claros y accionables
|
|
489
|
+
- ✅ Tests con > 80% de cobertura
|
|
490
|
+
|
|
491
|
+
## 📞 Risk Assessment
|
|
492
|
+
|
|
493
|
+
### Riesgos
|
|
494
|
+
| Riesgo | Probabilidad | Impacto | Mitigación |
|
|
495
|
+
|---------|-------------|-----------|-------------|
|
|
496
|
+
| Retry infinito con TF26071 persistente | Baja | Alta | Límite de 3 reintentos por defecto |
|
|
497
|
+
| Memory leak en colas no limpiadas | Baja | Media | Limpieza automática en `finally()` |
|
|
498
|
+
| Performance degradation con muchos retries | Media | Baja | Backoff exponencial y límite de reintentos |
|
|
499
|
+
|
|
500
|
+
## 📊 Estimación de Esfuerzo
|
|
501
|
+
|
|
502
|
+
### Complejidad Técnica: Media
|
|
503
|
+
- Implementación de sistema de cola: 2-3 días
|
|
504
|
+
- Lógica de retry con re-fetch: 1-2 días
|
|
505
|
+
- Integración y testing: 2-3 días
|
|
506
|
+
- Documentación: 1 día
|
|
507
|
+
|
|
508
|
+
**Total estimado**: 6-9 días de desarrollo
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
*Este PRD establece la solución completa para el problema de concurrencia en `ado_add_attachment`, eliminando los errores TF26071 y proporcionando una experiencia de usuario robusta y transparente.*
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Bug Fixes v2.4.2 - v2.4.5
|
|
2
|
+
|
|
3
|
+
Serie de correcciones críticas que mejoran la estabilidad y funcionalidad del MCP Azure DevOps.
|
|
4
|
+
|
|
5
|
+
## 🐛 Correcciones Incluidas
|
|
6
|
+
|
|
7
|
+
### v2.4.2
|
|
8
|
+
- **Version bump**: Preparación para serie de correcciones
|
|
9
|
+
- **Cambios**: Actualización de versión en package.json
|
|
10
|
+
|
|
11
|
+
### v2.4.3 (Commit 04e1f3a)
|
|
12
|
+
- **Fix(attachments)**: Clarify name parameter description in `ado_add_attachment`
|
|
13
|
+
|
|
14
|
+
#### 📝 Detalles
|
|
15
|
+
**Problema**: La descripción del parámetro `name` en la herramienta `ado_add_attachment` no era clara sobre su comportamiento.
|
|
16
|
+
|
|
17
|
+
**Solución**:
|
|
18
|
+
- Actualizar la descripción para aclarar que el parámetro `name` es opcional
|
|
19
|
+
- Documentar que por defecto usa el nombre del archivo original cuando no se especifica
|
|
20
|
+
- Mejorar la claridad de la documentación para los usuarios
|
|
21
|
+
|
|
22
|
+
**Código cambiado**:
|
|
23
|
+
```typescript
|
|
24
|
+
// Antes: "Nombre del archivo (opcional)"
|
|
25
|
+
// Después: "Nombre del archivo (opcional, se usa el nombre del archivo si no se especifica)"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### v2.4.4 (Commit 82d31fa)
|
|
29
|
+
- **Fix(attachments)**: Preserve fileName parameter in attachment URL
|
|
30
|
+
|
|
31
|
+
#### 📝 Detalles
|
|
32
|
+
**Problema Crítico**: Los nombres de archivos adjuntos no aparecían correctamente en Azure DevOps boards y listas.
|
|
33
|
+
|
|
34
|
+
**Causa Raíz**:
|
|
35
|
+
- Cuando se subía un adjunto, Azure DevOps devolvía una URL con parámetro `?fileName`
|
|
36
|
+
- Formato: `https://dev.azure.com/{org}/{project}/_apis/wit/attachments/{id}?fileName={name}`
|
|
37
|
+
- El MCP estaba descartando este parámetro al reconstruir la URL para vincular
|
|
38
|
+
|
|
39
|
+
**Impacto**:
|
|
40
|
+
- Los adjuntos aparecían sin nombre en Azure DevOps boards
|
|
41
|
+
- La experiencia de usuario era confusa
|
|
42
|
+
- La funcionalidad principal de adjuntos estaba comprometida
|
|
43
|
+
|
|
44
|
+
**Solución**:
|
|
45
|
+
```typescript
|
|
46
|
+
// uploadAttachmentRest: Agregar comentario clarificador
|
|
47
|
+
// La URL devuelta por Azure DevOps ya incluye el parámetro fileName
|
|
48
|
+
// formato: https://dev.azure.com/{org}/{project}/_apis/wit/attachments/{id}?fileName={name}
|
|
49
|
+
|
|
50
|
+
// ado_add_attachment: Usar URL completa devuelta por Azure DevOps
|
|
51
|
+
const attachment = await uploadAttachmentRest(filePath, fileName);
|
|
52
|
+
attachmentLinkUrl = attachment.url; // Usar la URL completa con ?fileName incluido
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Cambios Implementados**:
|
|
56
|
+
1. Modificar `uploadAttachmentRest` para agregar comentario sobre el formato de URL
|
|
57
|
+
2. Modificar `ado_add_attachment` para usar la URL completa devuelta por Azure DevOps
|
|
58
|
+
3. Declarar `attachmentLinkUrl` en el scope correcto para evitar errores de referencia
|
|
59
|
+
4. Agregar cláusula `else` para lanzar error cuando no se proporciona ni `filePath` ni `attachmentUrl`
|
|
60
|
+
|
|
61
|
+
**Resultado**:
|
|
62
|
+
- ✅ Los nombres de archivos ahora aparecen correctamente en Azure DevOps
|
|
63
|
+
- ✅ La experiencia de usuario es más clara
|
|
64
|
+
- ✅ La funcionalidad principal de adjuntos trabaja como se espera
|
|
65
|
+
|
|
66
|
+
### v2.4.5 (Commit 92b9db2)
|
|
67
|
+
- **Fix(env)**: Initialize currentProject with environment variable
|
|
68
|
+
|
|
69
|
+
#### 📝 Detalles
|
|
70
|
+
**Problema Crítico**: Race condition en llamadas REST cuando `currentProject` estaba vacío.
|
|
71
|
+
|
|
72
|
+
**Causa Raíz**:
|
|
73
|
+
```typescript
|
|
74
|
+
// Línea 320 (antes):
|
|
75
|
+
let currentProject: string = ""; // ❌ Inicializado como string vacío
|
|
76
|
+
|
|
77
|
+
// En autoConfigureFromEnv():
|
|
78
|
+
currentProject = project || ""; // Solo se llena cuando se llama
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Problema**:
|
|
82
|
+
- Las herramientas MCP podían ser llamadas antes de que `autoConfigureFromEnv()` completara
|
|
83
|
+
- Las llamadas REST en `uploadAttachmentRest()` fallaban cuando `currentProject` estaba vacío
|
|
84
|
+
- URL de construcción fallaba: `${baseUrl}//_apis/wit/attachments` (falta nombre del proyecto)
|
|
85
|
+
|
|
86
|
+
**Impacto**:
|
|
87
|
+
- Las herramientas MCP fallaban intermitentemente al inicio
|
|
88
|
+
- La experiencia de usuario era inconsistente
|
|
89
|
+
- Las operaciones con adjuntos fallaban con error de URL inválida
|
|
90
|
+
|
|
91
|
+
**Solución**:
|
|
92
|
+
```typescript
|
|
93
|
+
// Línea 91 (después):
|
|
94
|
+
let currentProject: string = ENV_ADO_PROJECT || ""; // ✅ Inicializar con variable de entorno
|
|
95
|
+
|
|
96
|
+
// Asegura que el nombre del proyecto está disponible desde la inicialización del módulo
|
|
97
|
+
// Elimina la condición de carrera con autoConfigureFromEnv()
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Cambios Implementados**:
|
|
101
|
+
1. Modificar inicialización de `currentProject` para usar `ENV_ADO_PROJECT || ""`
|
|
102
|
+
2. Asegurar disponibilidad del nombre del proyecto desde la carga del módulo
|
|
103
|
+
3. Eliminar condición de carrera con `autoConfigureFromEnv()`
|
|
104
|
+
|
|
105
|
+
**Resultado**:
|
|
106
|
+
- ✅ Las llamadas REST API funcionan correctamente con variables de entorno
|
|
107
|
+
- ✅ No más condiciones de carrera al inicio
|
|
108
|
+
- ✅ La experiencia de usuario es consistente desde el primer uso
|
|
109
|
+
|
|
110
|
+
## 📊 Impacto General
|
|
111
|
+
|
|
112
|
+
### Estabilidad
|
|
113
|
+
- **3 correcciones críticas** que mejoran la confiabilidad del MCP
|
|
114
|
+
- **Eliminación de bugs** que afectaban la experiencia de usuario principal
|
|
115
|
+
- **Mejoras en manejo de errores** para mensajes más claros
|
|
116
|
+
|
|
117
|
+
### Funcionalidad
|
|
118
|
+
- **Adjuntos**: Ahora funcionan correctamente con nombres apropiados
|
|
119
|
+
- **Configuración**: Eliminación de condiciones de carrera
|
|
120
|
+
- **Documentación**: Mejoras en claridad para usuarios
|
|
121
|
+
|
|
122
|
+
### Compatibilidad
|
|
123
|
+
- **100% compatible** con versiones anteriores
|
|
124
|
+
- **Sin breaking changes**
|
|
125
|
+
- **Migración transparente**: Solo actualizar la versión
|
|
126
|
+
|
|
127
|
+
## 🚀 Instalación
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npm install @slorenzot/mcp-azure@2.4.5
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## 🔄 Migración desde v2.4.1
|
|
134
|
+
|
|
135
|
+
Esta versión es **100% compatible** con v2.4.1. Simplemente actualiza:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npm update @slorenzot/mcp-azure
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## ✅ Testing Realizado
|
|
142
|
+
|
|
143
|
+
- ✅ Adjuntos ahora aparecen con nombres correctos en Azure DevOps boards
|
|
144
|
+
- ✅ Eliminación de condiciones de carrera al inicio del servidor
|
|
145
|
+
- ✅ Manejo correcto de variables de entorno
|
|
146
|
+
- ✅ Compatibilidad verificada con configuraciones existentes
|
|
147
|
+
- ✅ Mensajes de error mejorados para usuarios
|
|
148
|
+
|
|
149
|
+
## 🐛 Problemas Resueltos
|
|
150
|
+
|
|
151
|
+
- ❌ Adjuntos sin nombres en Azure DevOps boards (v2.4.4)
|
|
152
|
+
- ❌ Condiciones de carrera al inicio (v2.4.5)
|
|
153
|
+
- ❌ Descripción confusa de parámetro name (v2.4.3)
|
|
154
|
+
|
|
155
|
+
## 🔮 Roadmap
|
|
156
|
+
|
|
157
|
+
Esta versión establece una base sólida para v2.6.0 que incluirá:
|
|
158
|
+
- Sistema de configuración jerárquico (.mcp.json → variables de entorno)
|
|
159
|
+
- Manejo de errores robusto con validaciones avanzadas
|
|
160
|
+
- Codificación URL inteligente para nombres de proyecto
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
*Serie de correcciones críticas que transformaron la estabilidad y funcionalidad de adjuntos en el MCP Azure DevOps.*
|