@yemi33/minions 0.1.1901 → 0.1.1903
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/dashboard/js/render-watches.js +328 -45
- package/dashboard.js +18 -2
- package/engine/ado.js +74 -0
- package/engine/github.js +162 -1
- package/engine/queries.js +3 -0
- package/engine/safe-expr.js +350 -0
- package/engine/shared.js +40 -0
- package/engine/watch-actions.js +314 -8
- package/engine/watches.js +474 -30
- package/package.json +1 -1
|
@@ -51,6 +51,20 @@ const _WATCH_CONDITION_LABELS = {
|
|
|
51
51
|
enabled: 'Enabled',
|
|
52
52
|
disabled: 'Disabled',
|
|
53
53
|
'activity-change': 'Activity Change',
|
|
54
|
+
// P-w4e2f6a1 — Phase 2.1: PR predicate conditions
|
|
55
|
+
'head-commit-change': 'Head Commit Changed',
|
|
56
|
+
'mergeable-flipped': 'Mergeable Flipped',
|
|
57
|
+
'ready-for-merge': 'Ready For Merge',
|
|
58
|
+
'behind-master': 'Behind Master',
|
|
59
|
+
'draft-flipped': 'Draft Flipped',
|
|
60
|
+
// P-w5b8d2c9 — Phase 2.2: work-item / plan / pipeline predicate conditions
|
|
61
|
+
stalled: 'Stalled',
|
|
62
|
+
'retry-limit-reached': 'Retry Limit Reached',
|
|
63
|
+
'dependency-met': 'Dependency Met',
|
|
64
|
+
'all-items-done': 'All Items Done',
|
|
65
|
+
'item-failed-n-times': 'Item Failed N Times',
|
|
66
|
+
'stage-advanced': 'Stage Advanced',
|
|
67
|
+
'stuck-in-stage': 'Stuck In Stage',
|
|
54
68
|
};
|
|
55
69
|
|
|
56
70
|
function _conditionLabel(cond) {
|
|
@@ -176,6 +190,79 @@ function _watchNext() { var tp = Math.ceil((window._lastWatches || []).length /
|
|
|
176
190
|
|
|
177
191
|
// ─── Detail Modal ───────────────────────────────────────────────────────────
|
|
178
192
|
|
|
193
|
+
// P-w14e7a8c — Phase 7.1: render the action/chain block for the detail modal.
|
|
194
|
+
// Single-object action prints one step (parity with legacy view); array-form
|
|
195
|
+
// chain prints each step indexed. Each step surfaces guard.expr when present
|
|
196
|
+
// so users can verify the editor round-tripped through engine/watches.js.
|
|
197
|
+
function _renderWatchActionDetail(action) {
|
|
198
|
+
if (!action) return '';
|
|
199
|
+
if (Array.isArray(action)) {
|
|
200
|
+
if (action.length === 0) return '';
|
|
201
|
+
var stepHtml = action.map(function(step, i) {
|
|
202
|
+
return '<div style="margin-top:4px;padding:8px;background:var(--surface2);border-radius:4px">' +
|
|
203
|
+
'<div style="font-size:11px;color:var(--muted)">Step ' + (i + 1) + '</div>' +
|
|
204
|
+
'<div style="font-size:12px"><strong>Type:</strong> <span class="dispatch-type explore">' + escHtml(step && step.type ? step.type : '?') + '</span></div>' +
|
|
205
|
+
(step && step.guard && step.guard.expr ? '<div style="font-size:11px;margin-top:2px"><strong>Guard:</strong> <code>' + escHtml(step.guard.expr) + '</code></div>' : '') +
|
|
206
|
+
(step && step.params && Object.keys(step.params).length ? '<div style="margin-top:4px;font-size:11px;font-family:monospace;white-space:pre-wrap">' + escHtml(JSON.stringify(step.params, null, 2)) + '</div>' : '') +
|
|
207
|
+
'</div>';
|
|
208
|
+
}).join('');
|
|
209
|
+
return '<div><strong style="color:var(--muted)">Follow-up Action Chain (' + action.length + ' step' + (action.length === 1 ? '' : 's') + '):</strong>' + stepHtml + '</div>';
|
|
210
|
+
}
|
|
211
|
+
if (action.type) {
|
|
212
|
+
return '<div><strong style="color:var(--muted)">Follow-up Action:</strong> <span class="dispatch-type explore">' + escHtml(action.type) + '</span>' +
|
|
213
|
+
(action.guard && action.guard.expr ? '<div style="font-size:11px;margin-top:2px"><strong>Guard:</strong> <code>' + escHtml(action.guard.expr) + '</code></div>' : '') +
|
|
214
|
+
(action.params && Object.keys(action.params).length ? '<div style="margin-top:4px;padding:8px;background:var(--surface2);border-radius:4px;font-size:11px;font-family:monospace;white-space:pre-wrap">' + escHtml(JSON.stringify(action.params, null, 2)) + '</div>' : '') +
|
|
215
|
+
'</div>';
|
|
216
|
+
}
|
|
217
|
+
return '';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// P-w14e7a8c — Phase 7.1: render the cross-target requires[] block for the
|
|
221
|
+
// detail modal. Empty / missing requires renders nothing.
|
|
222
|
+
function _renderWatchRequiresDetail(requires) {
|
|
223
|
+
if (!Array.isArray(requires) || requires.length === 0) return '';
|
|
224
|
+
var rows = requires.map(function(r, i) {
|
|
225
|
+
return '<div style="margin-top:4px;padding:6px 8px;background:var(--surface2);border-radius:4px;font-size:11px">' +
|
|
226
|
+
'<span style="color:var(--muted)">[' + (i + 1) + ']</span> ' +
|
|
227
|
+
escHtml(r && r.target ? r.target : '?') +
|
|
228
|
+
' <span class="dispatch-type explore">' + escHtml(r && r.targetType ? r.targetType : '?') + '</span>' +
|
|
229
|
+
' <span style="color:var(--blue)">' + escHtml(_conditionLabel(r && r.condition ? r.condition : '?')) + '</span>' +
|
|
230
|
+
'</div>';
|
|
231
|
+
}).join('');
|
|
232
|
+
return '<div><strong style="color:var(--muted)">Requires (AND-join, ' + requires.length + '):</strong>' + rows + '</div>';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// P-w15f1d4b — Phase 7.2: render the evaluation history block for the detail
|
|
236
|
+
// modal. `history` is the Phase 6.2 `_history` array shape
|
|
237
|
+
// `[{checked_at, fired, reason, condition}]` (oldest-first, capped at 25 by
|
|
238
|
+
// the engine). The lazy fetcher in openWatchDetail injects the rendered HTML
|
|
239
|
+
// into a `watch-history-<id>` placeholder div once /api/watches/:id/history
|
|
240
|
+
// resolves. Empty / missing arrays render a "no checks yet" hint so legacy
|
|
241
|
+
// watches that were created before Phase 6.2 don't show a half-empty table.
|
|
242
|
+
function _renderWatchHistoryDetail(history) {
|
|
243
|
+
if (!Array.isArray(history) || history.length === 0) {
|
|
244
|
+
return '<div style="color:var(--muted);font-style:italic;font-size:11px">no history yet — watch has not been checked, or pre-dates the history feature</div>';
|
|
245
|
+
}
|
|
246
|
+
// Render newest-first for readability — the engine stores oldest-first.
|
|
247
|
+
var entries = history.slice().reverse();
|
|
248
|
+
var rows = entries.map(function(entry) {
|
|
249
|
+
var fired = !!(entry && entry.fired);
|
|
250
|
+
var ts = entry && entry.checked_at ? formatLocalDateTime(entry.checked_at) : '?';
|
|
251
|
+
var reason = entry && entry.reason ? entry.reason : '';
|
|
252
|
+
var indicator = fired
|
|
253
|
+
? '<span style="color:var(--green)" title="fired">✓ fired</span>'
|
|
254
|
+
: '<span style="color:var(--muted)" title="not fired">·</span>';
|
|
255
|
+
return '<tr>' +
|
|
256
|
+
'<td style="padding:2px 8px 2px 0;color:var(--muted);white-space:nowrap">' + escHtml(ts) + '</td>' +
|
|
257
|
+
'<td style="padding:2px 8px 2px 0;white-space:nowrap">' + indicator + '</td>' +
|
|
258
|
+
'<td style="padding:2px 0;color:var(--text)">' + escHtml(reason) + '</td>' +
|
|
259
|
+
'</tr>';
|
|
260
|
+
}).join('');
|
|
261
|
+
return '<div><strong style="color:var(--muted)">Evaluation History (' + history.length + ' check' + (history.length === 1 ? '' : 's') + ', newest first):</strong>' +
|
|
262
|
+
'<table style="margin-top:4px;font-size:11px;border-collapse:collapse;width:100%"><tbody>' + rows + '</tbody></table>' +
|
|
263
|
+
'</div>';
|
|
264
|
+
}
|
|
265
|
+
|
|
179
266
|
function openWatchDetail(id) {
|
|
180
267
|
var w = (window._lastWatches || []).find(function(x) { return x.id === id; });
|
|
181
268
|
if (!w) return;
|
|
@@ -204,9 +291,14 @@ function openWatchDetail(id) {
|
|
|
204
291
|
'<div><strong style="color:var(--muted)">Notify:</strong> ' + escHtml(w.notify || 'inbox') + '</div>' +
|
|
205
292
|
'<div><strong style="color:var(--muted)">Triggers:</strong> ' + (w.triggerCount || 0) + (w.stopAfter > 0 ? ' / ' + w.stopAfter + ' (expires after)' : ' (runs forever)') + '</div>' +
|
|
206
293
|
(w.onNotMet ? '<div><strong style="color:var(--muted)">On Each Poll (not met):</strong> ' + escHtml(w.onNotMet) + '</div>' : '') +
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
294
|
+
// P-w14e7a8c — Phase 7.1: render single action OR array-form chain.
|
|
295
|
+
// Each step shows its type, params, and (if present) guard expression
|
|
296
|
+
// so users can verify that the chain editor round-tripped through the
|
|
297
|
+
// back-end (engine/watches.js createWatch persists the shape unchanged).
|
|
298
|
+
_renderWatchActionDetail(w.action) +
|
|
299
|
+
// P-w14e7a8c — Phase 7.1: render cross-target requirements list when
|
|
300
|
+
// the watch carries a non-empty requires[] from the editor.
|
|
301
|
+
_renderWatchRequiresDetail(w.requires) +
|
|
210
302
|
(w._lastActionResult ? '<div><strong style="color:var(--muted)">Last Action Result:</strong> <span style="color:' + (w._lastActionResult.ok ? 'var(--green)' : 'var(--red)') + '">' + (w._lastActionResult.ok ? 'OK' : 'FAILED') + '</span> — ' + escHtml(w._lastActionResult.summary || '') + (w._lastActionResult.dispatchedItemId ? ' (dispatched: ' + escHtml(w._lastActionResult.dispatchedItemId) + ')' : '') + '</div>' : '') +
|
|
211
303
|
'<div><strong style="color:var(--muted)">Created:</strong> ' + escHtml(createdAt) + '</div>' +
|
|
212
304
|
'<div><strong style="color:var(--muted)">Last Checked:</strong> ' + escHtml(lastChecked) + '</div>' +
|
|
@@ -214,12 +306,38 @@ function openWatchDetail(id) {
|
|
|
214
306
|
(w._lastTriggerMessage ? '<div><strong style="color:var(--muted)">Last Trigger Message:</strong><div style="margin-top:4px;padding:8px;background:var(--surface2);border-radius:4px;font-size:11px">' + escHtml(w._lastTriggerMessage) + '</div></div>' : '') +
|
|
215
307
|
(w.project ? '<div><strong style="color:var(--muted)">Project:</strong> ' + escHtml(w.project) + '</div>' : '') +
|
|
216
308
|
(w.description ? '<div><strong style="color:var(--muted)">Description:</strong> ' + escHtml(w.description) + '</div>' : '') +
|
|
309
|
+
// P-w15f1d4b — Phase 7.2: reserve a placeholder for the lazily-fetched
|
|
310
|
+
// evaluation history table. The fetch below populates it once
|
|
311
|
+
// /api/watches/:id/history resolves; failures fall back to the inline
|
|
312
|
+
// _history copy from /api/watches (best-effort) or a "no history" hint.
|
|
313
|
+
'<div id="watch-history-' + escHtml(w.id) + '"><div style="color:var(--muted);font-style:italic;font-size:11px">Loading history…</div></div>' +
|
|
217
314
|
'</div>';
|
|
218
315
|
|
|
219
316
|
document.getElementById('modal-body').innerHTML = body;
|
|
220
317
|
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
221
318
|
document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
222
319
|
document.getElementById('modal').classList.add('open');
|
|
320
|
+
|
|
321
|
+
// P-w15f1d4b — Phase 7.2: lazy history fetch. We don't block modal render
|
|
322
|
+
// on this — the placeholder above is replaced once /api/watches/:id/history
|
|
323
|
+
// resolves. On failure, fall back to whatever _history is already inline
|
|
324
|
+
// on the watch object (often present from the most recent /api/watches
|
|
325
|
+
// poll) so legacy watches still surface what they have locally.
|
|
326
|
+
(function loadHistory() {
|
|
327
|
+
var container = document.getElementById('watch-history-' + w.id);
|
|
328
|
+
if (!container) return;
|
|
329
|
+
fetch('/api/watches/' + encodeURIComponent(w.id) + '/history').then(function(res) {
|
|
330
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
331
|
+
return res.json();
|
|
332
|
+
}).then(function(data) {
|
|
333
|
+
if (!container.isConnected) return; // user closed the modal
|
|
334
|
+
container.innerHTML = _renderWatchHistoryDetail(data && data.history);
|
|
335
|
+
}).catch(function() {
|
|
336
|
+
if (!container.isConnected) return;
|
|
337
|
+
// Fall back to the inline _history from /api/watches (best-effort).
|
|
338
|
+
container.innerHTML = _renderWatchHistoryDetail(w._history || []);
|
|
339
|
+
});
|
|
340
|
+
})();
|
|
223
341
|
}
|
|
224
342
|
|
|
225
343
|
// ─── CRUD Actions ───────────────────────────────────────────────────────────
|
|
@@ -290,14 +408,6 @@ function _watchFormHtml() {
|
|
|
290
408
|
var agentOpts = '<option value="">human</option>' + (cmdAgents || []).map(function(a) { return '<option value="' + escHtml(a.id) + '">' + escHtml(a.name) + '</option>'; }).join('');
|
|
291
409
|
var projOpts = '<option value="">Any</option>' + (cmdProjects || []).map(function(p) { return '<option value="' + escHtml(p.name) + '">' + escHtml(p.name) + '</option>'; }).join('');
|
|
292
410
|
|
|
293
|
-
// Action picker: empty value = no follow-up action (legacy notify-only behavior).
|
|
294
|
-
var actionOpts = '<option value="">None — notify only</option>';
|
|
295
|
-
if (_watchActionTypesCache && _watchActionTypesCache.length) {
|
|
296
|
-
actionOpts += _watchActionTypesCache.map(function(a) {
|
|
297
|
-
return '<option value="' + escHtml(a.value) + '" title="' + escHtml(a.description || '') + '">' + escHtml(a.label) + '</option>';
|
|
298
|
-
}).join('');
|
|
299
|
-
}
|
|
300
|
-
|
|
301
411
|
return '<div style="display:flex;flex-direction:column;gap:12px;font-family:inherit">' +
|
|
302
412
|
'<div id="watch-form-error" style="display:none;color:var(--red);font-size:12px;padding:6px 10px;background:rgba(255,50,50,0.1);border-radius:var(--radius-sm)"></div>' +
|
|
303
413
|
'<label style="color:var(--text);font-size:var(--text-md)">Target (e.g. PR number, work item ID, meeting/plan/schedule/pipeline/dispatch/agent ID)<input id="watch-edit-target" placeholder="e.g. 1057, W-abc123, M-xyz, schedule-42" style="' + inputStyle + '"></label>' +
|
|
@@ -309,19 +419,82 @@ function _watchFormHtml() {
|
|
|
309
419
|
'<label style="color:var(--text);font-size:var(--text-md)">Description<input id="watch-edit-desc" placeholder="Optional description" style="' + inputStyle + '"></label>' +
|
|
310
420
|
'<label style="color:var(--text);font-size:var(--text-md)">Stop After N Triggers <span style="font-size:10px;color:var(--muted)">(0 = run forever, 1 = expire on first match)</span><input id="watch-edit-stop-after" type="number" value="0" min="0" style="' + inputStyle + '"></label>' +
|
|
311
421
|
'<label style="color:var(--text);font-size:var(--text-md)">On Each Poll (if condition not met)<select id="watch-edit-on-not-met" style="' + inputStyle + '"><option value="">None — do nothing</option><option value="notify">Notify — write to inbox each poll</option></select></label>' +
|
|
312
|
-
|
|
313
|
-
'<
|
|
422
|
+
// P-w14e7a8c — Phase 7.1: action chain editor (step list + add/remove)
|
|
423
|
+
'<div style="border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px;display:flex;flex-direction:column;gap:8px">' +
|
|
424
|
+
'<div style="color:var(--text);font-size:var(--text-md);display:flex;align-items:center;justify-content:space-between">' +
|
|
425
|
+
'<span>Follow-up Action(s) <span style="font-size:10px;color:var(--muted)">(runs after the inbox notification when the watch fires; 1 step = single, 2+ = chain)</span></span>' +
|
|
426
|
+
'<button type="button" class="pr-pager-btn" style="font-size:10px;padding:2px 8px;color:var(--green);border-color:var(--green)" onclick="_addWatchStepRow()">+ Add step</button>' +
|
|
427
|
+
'</div>' +
|
|
428
|
+
'<div id="watch-edit-steps"></div>' +
|
|
429
|
+
'</div>' +
|
|
430
|
+
// P-w14e7a8c — Phase 7.1: requires editor (cross-target AND-join rows)
|
|
431
|
+
'<div style="border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px;display:flex;flex-direction:column;gap:8px">' +
|
|
432
|
+
'<div style="color:var(--text);font-size:var(--text-md);display:flex;align-items:center;justify-content:space-between">' +
|
|
433
|
+
'<span>Requires <span style="font-size:10px;color:var(--muted)">(cross-target AND-join — every requirement must also be true to fire)</span></span>' +
|
|
434
|
+
'<button type="button" class="pr-pager-btn" style="font-size:10px;padding:2px 8px;color:var(--green);border-color:var(--green)" onclick="_addRequireRow()">+ Add requirement</button>' +
|
|
435
|
+
'</div>' +
|
|
436
|
+
'<div id="watch-edit-requires"></div>' +
|
|
437
|
+
'</div>' +
|
|
314
438
|
'</div>';
|
|
315
439
|
}
|
|
316
440
|
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
441
|
+
// P-w14e7a8c — Phase 7.1: action chain step editor.
|
|
442
|
+
// Renders a single step block with type / params / guard inputs into the
|
|
443
|
+
// container at #watch-edit-steps. Step indexes are stable per-block so the
|
|
444
|
+
// remove button can target the right node; the submit handler walks live
|
|
445
|
+
// rows (not these indexes) and packs them into a single object (1 step) or
|
|
446
|
+
// array (2+ steps) before sending to /api/watches.
|
|
447
|
+
var _watchStepSeq = 0;
|
|
448
|
+
function _addWatchStepRow(initial) {
|
|
449
|
+
var container = document.getElementById('watch-edit-steps');
|
|
450
|
+
if (!container) return;
|
|
451
|
+
_watchStepSeq += 1;
|
|
452
|
+
var idx = _watchStepSeq;
|
|
453
|
+
var inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit';
|
|
454
|
+
var actionOpts = '<option value="">— select action —</option>';
|
|
455
|
+
if (_watchActionTypesCache && _watchActionTypesCache.length) {
|
|
456
|
+
actionOpts += _watchActionTypesCache.map(function(a) {
|
|
457
|
+
return '<option value="' + escHtml(a.value) + '" title="' + escHtml(a.description || '') + '">' + escHtml(a.label) + '</option>';
|
|
458
|
+
}).join('');
|
|
459
|
+
}
|
|
460
|
+
var initType = (initial && initial.type) || '';
|
|
461
|
+
var initParams = initial && initial.params ? JSON.stringify(initial.params, null, 2) : '';
|
|
462
|
+
var initGuard = initial && initial.guard && initial.guard.expr ? initial.guard.expr : '';
|
|
463
|
+
var row = document.createElement('div');
|
|
464
|
+
row.className = 'watch-step-row';
|
|
465
|
+
row.setAttribute('data-step-idx', String(idx));
|
|
466
|
+
row.style.cssText = 'border:1px solid var(--border);border-radius:var(--radius-sm);padding:8px;display:flex;flex-direction:column;gap:6px;background:var(--surface)';
|
|
467
|
+
row.innerHTML =
|
|
468
|
+
'<div style="display:flex;align-items:center;justify-content:space-between;gap:8px">' +
|
|
469
|
+
'<select id="watch-step-' + idx + '-type" style="' + inputStyle + ';margin-top:0;flex:1" onchange="_updateWatchStepParamsHint(' + idx + ')">' + actionOpts + '</select>' +
|
|
470
|
+
'<button type="button" class="pr-pager-btn" style="font-size:10px;padding:2px 8px;color:var(--red);border-color:var(--red)" onclick="_removeWatchStepRow(' + idx + ')">Remove</button>' +
|
|
471
|
+
'</div>' +
|
|
472
|
+
'<label style="color:var(--text);font-size:11px">Params (JSON) <span style="font-size:10px;color:var(--muted)" id="watch-step-' + idx + '-params-hint"></span>' +
|
|
473
|
+
'<textarea id="watch-step-' + idx + '-params" rows="3" placeholder=\'{"title": "Re-check {{target}} after merge"}\' style="' + inputStyle + ';font-family:monospace"></textarea>' +
|
|
474
|
+
'</label>' +
|
|
475
|
+
'<label style="color:var(--text);font-size:11px">Guard expression <span style="font-size:10px;color:var(--muted)">(optional — skip step if expr is false; e.g. <code>newState.mergeable === true</code>)</span>' +
|
|
476
|
+
'<input id="watch-step-' + idx + '-guard" placeholder="" style="' + inputStyle + ';font-family:monospace">' +
|
|
477
|
+
'</label>';
|
|
478
|
+
container.appendChild(row);
|
|
479
|
+
if (initType) document.getElementById('watch-step-' + idx + '-type').value = initType;
|
|
480
|
+
if (initParams) document.getElementById('watch-step-' + idx + '-params').value = initParams;
|
|
481
|
+
if (initGuard) document.getElementById('watch-step-' + idx + '-guard').value = initGuard;
|
|
482
|
+
_updateWatchStepParamsHint(idx);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function _removeWatchStepRow(idx) {
|
|
486
|
+
var row = document.querySelector('.watch-step-row[data-step-idx="' + idx + '"]');
|
|
487
|
+
if (row && row.parentNode) row.parentNode.removeChild(row);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Refresh the hint next to a single step's params textarea based on its
|
|
491
|
+
// currently selected action type. Same registry-driven hint as the legacy
|
|
492
|
+
// single-action form (kept for parity).
|
|
493
|
+
function _updateWatchStepParamsHint(idx) {
|
|
494
|
+
var sel = document.getElementById('watch-step-' + idx + '-type');
|
|
495
|
+
var hint = document.getElementById('watch-step-' + idx + '-params-hint');
|
|
496
|
+
if (!sel || !hint) return;
|
|
497
|
+
if (!sel.value) { hint.textContent = ''; return; }
|
|
325
498
|
var entry = (_watchActionTypesCache || []).find(function(a) { return a.value === sel.value; });
|
|
326
499
|
if (!entry) { hint.textContent = ''; return; }
|
|
327
500
|
var paramHints = (entry.params || []).map(function(p) {
|
|
@@ -330,6 +503,60 @@ function _updateWatchActionParamsHint() {
|
|
|
330
503
|
hint.textContent = paramHints ? '— ' + paramHints : '';
|
|
331
504
|
}
|
|
332
505
|
|
|
506
|
+
// P-w14e7a8c — Phase 7.1: requires-row editor.
|
|
507
|
+
// Each row is one cross-target requirement: {target, targetType, condition}.
|
|
508
|
+
// Per the engine contract (engine/watches.js _validateRequires) requirements
|
|
509
|
+
// MUST NOT themselves declare nested requires — the form intentionally
|
|
510
|
+
// surfaces only the three fields the back-end accepts.
|
|
511
|
+
var _watchRequireSeq = 0;
|
|
512
|
+
function _addRequireRow(initial) {
|
|
513
|
+
var container = document.getElementById('watch-edit-requires');
|
|
514
|
+
if (!container) return;
|
|
515
|
+
_watchRequireSeq += 1;
|
|
516
|
+
var idx = _watchRequireSeq;
|
|
517
|
+
var inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit';
|
|
518
|
+
var targetTypes = _watchTargetTypesCache && _watchTargetTypesCache.length ? _watchTargetTypesCache : [];
|
|
519
|
+
var ttOpts = '<option value="">— targetType —</option>' + targetTypes.map(function(t) {
|
|
520
|
+
return '<option value="' + escHtml(t.value) + '">' + escHtml(t.label) + '</option>';
|
|
521
|
+
}).join('');
|
|
522
|
+
var initTarget = (initial && initial.target) || '';
|
|
523
|
+
var initTT = (initial && initial.targetType) || '';
|
|
524
|
+
var initCond = (initial && initial.condition) || '';
|
|
525
|
+
var row = document.createElement('div');
|
|
526
|
+
row.className = 'watch-require-row';
|
|
527
|
+
row.setAttribute('data-require-idx', String(idx));
|
|
528
|
+
row.style.cssText = 'display:grid;grid-template-columns:1fr 1fr 1fr auto;gap:6px;align-items:end';
|
|
529
|
+
row.innerHTML =
|
|
530
|
+
'<label style="color:var(--text);font-size:11px;margin:0">Target<input id="watch-require-' + idx + '-target" placeholder="e.g. PR-123, pipeline-X" style="' + inputStyle + '"></label>' +
|
|
531
|
+
'<label style="color:var(--text);font-size:11px;margin:0">Target Type<select id="watch-require-' + idx + '-target-type" style="' + inputStyle + '" onchange="_updateRequireConditionOptions(' + idx + ')">' + ttOpts + '</select></label>' +
|
|
532
|
+
'<label style="color:var(--text);font-size:11px;margin:0">Condition<select id="watch-require-' + idx + '-condition" style="' + inputStyle + '"></select></label>' +
|
|
533
|
+
'<button type="button" class="pr-pager-btn" style="font-size:10px;padding:6px 8px;color:var(--red);border-color:var(--red)" onclick="_removeRequireRow(' + idx + ')">Remove</button>';
|
|
534
|
+
container.appendChild(row);
|
|
535
|
+
if (initTarget) document.getElementById('watch-require-' + idx + '-target').value = initTarget;
|
|
536
|
+
if (initTT) document.getElementById('watch-require-' + idx + '-target-type').value = initTT;
|
|
537
|
+
_updateRequireConditionOptions(idx);
|
|
538
|
+
if (initCond) document.getElementById('watch-require-' + idx + '-condition').value = initCond;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function _removeRequireRow(idx) {
|
|
542
|
+
var row = document.querySelector('.watch-require-row[data-require-idx="' + idx + '"]');
|
|
543
|
+
if (row && row.parentNode) row.parentNode.removeChild(row);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Per-row condition refresh — mirrors _updateWatchConditionOptions for the
|
|
547
|
+
// primary watch's condition select but scoped to the requirement row.
|
|
548
|
+
function _updateRequireConditionOptions(idx) {
|
|
549
|
+
var ttSel = document.getElementById('watch-require-' + idx + '-target-type');
|
|
550
|
+
var condSel = document.getElementById('watch-require-' + idx + '-condition');
|
|
551
|
+
if (!ttSel || !condSel || !_watchTargetTypesCache) return;
|
|
552
|
+
var entry = _watchTargetTypesCache.find(function(t) { return t.value === ttSel.value; });
|
|
553
|
+
var conditions = (entry && entry.conditions) || [];
|
|
554
|
+
condSel.innerHTML = (conditions.length ? '' : '<option value="">— select target type first —</option>') +
|
|
555
|
+
conditions.map(function(c) {
|
|
556
|
+
return '<option value="' + escHtml(c) + '">' + escHtml(_conditionLabel(c)) + '</option>';
|
|
557
|
+
}).join('');
|
|
558
|
+
}
|
|
559
|
+
|
|
333
560
|
// Refresh the condition <select> based on the currently selected target type.
|
|
334
561
|
function _updateWatchConditionOptions() {
|
|
335
562
|
var ttSel = document.getElementById('watch-edit-target-type');
|
|
@@ -349,6 +576,11 @@ function _renderCreateWatchModal() {
|
|
|
349
576
|
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
350
577
|
document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
351
578
|
document.getElementById('modal').classList.add('open');
|
|
579
|
+
// P-w14e7a8c — Phase 7.1: seed one empty action step row so the picker is
|
|
580
|
+
// visible by default (matches today's UX where the action <select> is
|
|
581
|
+
// always visible). Users who want notify-only just leave the type empty
|
|
582
|
+
// (or click Remove); users who want a chain click "+ Add step" again.
|
|
583
|
+
_addWatchStepRow();
|
|
352
584
|
}
|
|
353
585
|
|
|
354
586
|
function openCreateWatchModal() {
|
|
@@ -389,44 +621,84 @@ function submitWatch() {
|
|
|
389
621
|
var description = (document.getElementById('watch-edit-desc') || {}).value || '';
|
|
390
622
|
var stopAfter = parseInt((document.getElementById('watch-edit-stop-after') || {}).value, 10) || 0;
|
|
391
623
|
var onNotMet = (document.getElementById('watch-edit-on-not-met') || {}).value || '';
|
|
392
|
-
var actionType = (document.getElementById('watch-edit-action-type') || {}).value || '';
|
|
393
|
-
var actionParamsRaw = (document.getElementById('watch-edit-action-params') || {}).value || '';
|
|
394
624
|
|
|
395
625
|
var errEl = document.getElementById('watch-form-error');
|
|
396
626
|
function showErr(msg) { if (errEl) { errEl.textContent = msg; errEl.style.display = 'block'; } }
|
|
397
627
|
|
|
398
628
|
if (!target.trim()) { showErr('Target is required'); return; }
|
|
399
629
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
630
|
+
// P-w14e7a8c — Phase 7.1: harvest action steps from the chain editor.
|
|
631
|
+
// 0 steps -> action: null (notify-only legacy behavior).
|
|
632
|
+
// 1 step -> action: { type, params, guard? } (single-object form).
|
|
633
|
+
// 2+ steps -> action: [ {...}, ... ] (array-form chain — engine/watch-actions.js
|
|
634
|
+
// runWatchActionChain handles it).
|
|
635
|
+
var stepRows = document.querySelectorAll('.watch-step-row');
|
|
636
|
+
var steps = [];
|
|
637
|
+
for (var i = 0; i < stepRows.length; i++) {
|
|
638
|
+
var row = stepRows[i];
|
|
639
|
+
var stepIdx = row.getAttribute('data-step-idx');
|
|
640
|
+
var typeEl = document.getElementById('watch-step-' + stepIdx + '-type');
|
|
641
|
+
var paramsEl = document.getElementById('watch-step-' + stepIdx + '-params');
|
|
642
|
+
var guardEl = document.getElementById('watch-step-' + stepIdx + '-guard');
|
|
643
|
+
var sType = typeEl && typeEl.value ? typeEl.value : '';
|
|
644
|
+
if (!sType) continue; // empty type → blank row, skip silently
|
|
645
|
+
var sParamsRaw = paramsEl ? paramsEl.value : '';
|
|
646
|
+
var sParams = {};
|
|
647
|
+
if (sParamsRaw && sParamsRaw.trim()) {
|
|
648
|
+
try { sParams = JSON.parse(sParamsRaw); }
|
|
649
|
+
catch (e) { showErr('Step ' + (steps.length + 1) + ' params must be valid JSON: ' + e.message); return; }
|
|
650
|
+
if (sParams === null || typeof sParams !== 'object' || Array.isArray(sParams)) {
|
|
651
|
+
showErr('Step ' + (steps.length + 1) + ' params must be a JSON object'); return;
|
|
408
652
|
}
|
|
409
653
|
}
|
|
410
|
-
|
|
654
|
+
var step = { type: sType, params: sParams };
|
|
655
|
+
var sGuard = guardEl && guardEl.value ? guardEl.value.trim() : '';
|
|
656
|
+
if (sGuard) step.guard = { expr: sGuard };
|
|
657
|
+
steps.push(step);
|
|
411
658
|
}
|
|
659
|
+
var action = null;
|
|
660
|
+
if (steps.length === 1) action = steps[0];
|
|
661
|
+
else if (steps.length > 1) action = steps;
|
|
662
|
+
|
|
663
|
+
// P-w14e7a8c — Phase 7.1: harvest cross-target requirements. Skip rows
|
|
664
|
+
// with no target (treat as empty/blank). Send `requires: []` (omit) when
|
|
665
|
+
// there are no usable rows so the back-end stays in legacy single-watch
|
|
666
|
+
// shape — engine/watches.js _validateRequires accepts both null and [].
|
|
667
|
+
var reqRows = document.querySelectorAll('.watch-require-row');
|
|
668
|
+
var requires = [];
|
|
669
|
+
for (var j = 0; j < reqRows.length; j++) {
|
|
670
|
+
var rrow = reqRows[j];
|
|
671
|
+
var rIdx = rrow.getAttribute('data-require-idx');
|
|
672
|
+
var rTarget = (document.getElementById('watch-require-' + rIdx + '-target') || {}).value || '';
|
|
673
|
+
var rType = (document.getElementById('watch-require-' + rIdx + '-target-type') || {}).value || '';
|
|
674
|
+
var rCond = (document.getElementById('watch-require-' + rIdx + '-condition') || {}).value || '';
|
|
675
|
+
if (!rTarget.trim() && !rType && !rCond) continue; // wholly blank row, skip
|
|
676
|
+
if (!rTarget.trim()) { showErr('Requirement ' + (requires.length + 1) + ': target is required'); return; }
|
|
677
|
+
if (!rType) { showErr('Requirement ' + (requires.length + 1) + ': target type is required'); return; }
|
|
678
|
+
if (!rCond) { showErr('Requirement ' + (requires.length + 1) + ': condition is required'); return; }
|
|
679
|
+
requires.push({ target: rTarget.trim(), targetType: rType, condition: rCond });
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
var payload = {
|
|
683
|
+
target: target.trim(),
|
|
684
|
+
targetType: targetType,
|
|
685
|
+
condition: condition,
|
|
686
|
+
interval: interval,
|
|
687
|
+
owner: owner || 'human',
|
|
688
|
+
project: project || null,
|
|
689
|
+
description: description || null,
|
|
690
|
+
notify: 'inbox',
|
|
691
|
+
stopAfter: stopAfter,
|
|
692
|
+
onNotMet: onNotMet || null,
|
|
693
|
+
action: action,
|
|
694
|
+
};
|
|
695
|
+
if (requires.length > 0) payload.requires = requires;
|
|
412
696
|
|
|
413
697
|
showToast('cmd-toast', 'Creating watch...', true);
|
|
414
698
|
fetch('/api/watches', {
|
|
415
699
|
method: 'POST',
|
|
416
700
|
headers: { 'Content-Type': 'application/json' },
|
|
417
|
-
body: JSON.stringify(
|
|
418
|
-
target: target.trim(),
|
|
419
|
-
targetType: targetType,
|
|
420
|
-
condition: condition,
|
|
421
|
-
interval: interval,
|
|
422
|
-
owner: owner || 'human',
|
|
423
|
-
project: project || null,
|
|
424
|
-
description: description || null,
|
|
425
|
-
notify: 'inbox',
|
|
426
|
-
stopAfter: stopAfter,
|
|
427
|
-
onNotMet: onNotMet || null,
|
|
428
|
-
action: action,
|
|
429
|
-
})
|
|
701
|
+
body: JSON.stringify(payload)
|
|
430
702
|
}).then(async function(res) {
|
|
431
703
|
var data = await res.json().catch(function() { return {}; });
|
|
432
704
|
if (!res.ok || data.error) {
|
|
@@ -453,4 +725,15 @@ window.MinionsWatches = {
|
|
|
453
725
|
_watchPrev: _watchPrev,
|
|
454
726
|
_watchNext: _watchNext,
|
|
455
727
|
_updateWatchConditionOptions: _updateWatchConditionOptions,
|
|
728
|
+
// P-w14e7a8c — Phase 7.1: chain + requires editor handlers (called from
|
|
729
|
+
// inline onclick attributes generated by _watchFormHtml / _addWatchStepRow
|
|
730
|
+
// / _addRequireRow). Exposed on the namespace for testability.
|
|
731
|
+
_addWatchStepRow: _addWatchStepRow,
|
|
732
|
+
_removeWatchStepRow: _removeWatchStepRow,
|
|
733
|
+
_updateWatchStepParamsHint: _updateWatchStepParamsHint,
|
|
734
|
+
_addRequireRow: _addRequireRow,
|
|
735
|
+
_removeRequireRow: _removeRequireRow,
|
|
736
|
+
_updateRequireConditionOptions: _updateRequireConditionOptions,
|
|
737
|
+
// P-w15f1d4b — Phase 7.2: history table helper exposed for testability.
|
|
738
|
+
_renderWatchHistoryDetail: _renderWatchHistoryDetail,
|
|
456
739
|
};
|
package/dashboard.js
CHANGED
|
@@ -6501,12 +6501,26 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6501
6501
|
return jsonReply(res, 200, { actionTypes: watchesMod.listActionTypes() });
|
|
6502
6502
|
}
|
|
6503
6503
|
|
|
6504
|
+
// P-w13c2b6f — Phase 6.3: surface the per-watch evaluation history
|
|
6505
|
+
// (Phase 6.2's `_history` array) over an HTTP GET. Returns
|
|
6506
|
+
// {id, history:[…]} for known ids and 404 otherwise. The handler is
|
|
6507
|
+
// exported as `_handleWatchHistory` so unit tests can call it with a
|
|
6508
|
+
// mock res without booting the full server.
|
|
6509
|
+
async function handleWatchHistory(req, res, match) {
|
|
6510
|
+
const id = match && match[1];
|
|
6511
|
+
if (!id) return jsonReply(res, 400, { error: 'watch id required' });
|
|
6512
|
+
const watch = watchesMod.getWatches().find(w => w.id === id);
|
|
6513
|
+
if (!watch) return jsonReply(res, 404, { error: 'Watch not found' });
|
|
6514
|
+
const history = Array.isArray(watch._history) ? watch._history : [];
|
|
6515
|
+
return jsonReply(res, 200, { id, history });
|
|
6516
|
+
}
|
|
6517
|
+
|
|
6504
6518
|
async function handleWatchesCreate(req, res) {
|
|
6505
6519
|
const body = await readBody(req);
|
|
6506
|
-
const { target, targetType, condition, interval, owner, description, project, notify, stopAfter, onNotMet, action } = body;
|
|
6520
|
+
const { target, targetType, condition, interval, owner, description, project, notify, stopAfter, onNotMet, action, requires } = body;
|
|
6507
6521
|
if (!target) return jsonReply(res, 400, { error: 'target is required' });
|
|
6508
6522
|
try {
|
|
6509
|
-
const watch = watchesMod.createWatch({ target, targetType, condition, interval, owner, description, project, notify, stopAfter, onNotMet, action });
|
|
6523
|
+
const watch = watchesMod.createWatch({ target, targetType, condition, interval, owner, description, project, notify, stopAfter, onNotMet, action, requires });
|
|
6510
6524
|
invalidateStatusCache();
|
|
6511
6525
|
recordCcTurnIfPresent(req, {
|
|
6512
6526
|
kind: 'watch', id: watch?.id || null, title: `Watch ${target}` + (condition ? ` (${condition})` : ''), project: project || null,
|
|
@@ -7536,6 +7550,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7536
7550
|
{ method: 'GET', path: '/api/watches', desc: 'List all watches', handler: handleWatchesList },
|
|
7537
7551
|
{ method: 'GET', path: '/api/watches/target-types', desc: 'List registered watch target types and their valid conditions', handler: handleWatchesTargetTypes },
|
|
7538
7552
|
{ method: 'GET', path: '/api/watches/action-types', desc: 'List registered follow-up action types (notify, dispatch-work-item, webhook, ...)', handler: handleWatchesActionTypes },
|
|
7553
|
+
{ method: 'GET', path: /^\/api\/watches\/([\w-]+)\/history$/, template: '/api/watches/:id/history', desc: 'Read the persisted evaluation history (last 25 checks) for a watch', handler: handleWatchHistory },
|
|
7539
7554
|
{ method: 'POST', path: '/api/watches', desc: 'Create a new watch', params: 'target, targetType, condition, interval?, owner?, description?, project?, notify?, stopAfter?, onNotMet?, action?', handler: handleWatchesCreate },
|
|
7540
7555
|
{ method: 'POST', path: '/api/watches/update', desc: 'Update a watch (pause/resume/modify)', params: 'id, status?, interval?, description?, notify?, stopAfter?, onNotMet?, condition?, action?', handler: handleWatchesUpdate },
|
|
7541
7556
|
{ method: 'POST', path: '/api/watches/delete', desc: 'Delete a watch', params: 'id', handler: handleWatchesDelete },
|
|
@@ -7942,6 +7957,7 @@ module.exports = {
|
|
|
7942
7957
|
_collectArchivedWorkItems: collectArchivedWorkItems,
|
|
7943
7958
|
buildCCStatePreamble,
|
|
7944
7959
|
_routesAsMeta,
|
|
7960
|
+
_server: server,
|
|
7945
7961
|
_buildTranscriptCarryover,
|
|
7946
7962
|
_transcriptHasCarryoverContext,
|
|
7947
7963
|
_ccRuntimeNeedsResumeCarryover,
|
package/engine/ado.js
CHANGED
|
@@ -746,6 +746,70 @@ async function pollPrStatus(config) {
|
|
|
746
746
|
updated = true;
|
|
747
747
|
}
|
|
748
748
|
|
|
749
|
+
// P-w1a3f9b2 — Phase 1.1: plumb mergeable / isDraft / mergeStatus /
|
|
750
|
+
// headRefOid onto the PR object so watches captureState (engine/watches.js)
|
|
751
|
+
// and future predicates (Phase 2.1: head-commit-change, mergeable-flipped,
|
|
752
|
+
// ready-for-merge, draft-flipped) can read them.
|
|
753
|
+
//
|
|
754
|
+
// ADO ↔ GitHub mapping notes:
|
|
755
|
+
// - `headRefOid` ← lastMergeSourceCommit.commitId (source-branch tip,
|
|
756
|
+
// equivalent to GitHub's pulls API head.sha — the source-side push tip,
|
|
757
|
+
// not the merge commit).
|
|
758
|
+
// - `mergeable` is derived from prData.mergeStatus, which is ADO's
|
|
759
|
+
// "succeeded | conflicts | failure | queued | notSet | rejectedByPolicy"
|
|
760
|
+
// enum. We collapse it to GitHub's tri-state contract:
|
|
761
|
+
// succeeded → true (mergeable)
|
|
762
|
+
// conflicts | failure | rejectedByPolicy → false (not mergeable)
|
|
763
|
+
// queued | notSet | (anything else) → null (computing / unknown,
|
|
764
|
+
// treated as "no signal"
|
|
765
|
+
// by predicates).
|
|
766
|
+
// - `mergeStatus` is preserved verbatim as the ADO-native equivalent of
|
|
767
|
+
// GitHub's `mergeStateStatus`. **Gap:** ADO does not expose a
|
|
768
|
+
// direct equivalent of GitHub's `mergeable_state` (clean/dirty/unstable/
|
|
769
|
+
// blocked/behind/draft/has_hooks/unknown), so `pr.mergeStateStatus`
|
|
770
|
+
// stays unset for ADO PRs and watches that depend on it (Phase 2.1
|
|
771
|
+
// `behind-master` / fine-grained block detection) need to fall back to
|
|
772
|
+
// `mergeable === false` plus `pr.behindBy` (Phase 1.3) when
|
|
773
|
+
// `pr.mergeStateStatus` is null/undefined.
|
|
774
|
+
// - `isDraft` ← prData.isDraft (ADO field, same boolean semantics as
|
|
775
|
+
// GitHub's `draft`).
|
|
776
|
+
// - `behindBy` (P-w3a7b9c4 — Phase 1.3): **Gap:** ADO has no direct
|
|
777
|
+
// `behind_by` count equivalent to GitHub's /compare API, so ADO PRs
|
|
778
|
+
// always get `behindBy: null`. A future implementation could walk
|
|
779
|
+
// `lastMergeTargetCommit → master` via the commit-graph; deferred
|
|
780
|
+
// for now (see prd open_questions on ADO behindBy parity). The
|
|
781
|
+
// `behind-master` predicate treats null as "not behind" so this
|
|
782
|
+
// null contract keeps ADO PRs dormant against that watch (no
|
|
783
|
+
// false positives) instead of over-firing.
|
|
784
|
+
if (sourceCommit && pr.headRefOid !== sourceCommit) {
|
|
785
|
+
pr.headRefOid = sourceCommit;
|
|
786
|
+
updated = true;
|
|
787
|
+
}
|
|
788
|
+
const adoMergeStatus = typeof prData.mergeStatus === 'string' ? prData.mergeStatus : null;
|
|
789
|
+
if ((pr.mergeStatus || null) !== adoMergeStatus) {
|
|
790
|
+
pr.mergeStatus = adoMergeStatus;
|
|
791
|
+
updated = true;
|
|
792
|
+
}
|
|
793
|
+
const newMergeable = adoMergeStatus === 'succeeded' ? true
|
|
794
|
+
: (adoMergeStatus === 'conflicts' || adoMergeStatus === 'failure' || adoMergeStatus === 'rejectedByPolicy') ? false
|
|
795
|
+
: null; // queued | notSet | unknown → no signal
|
|
796
|
+
if (pr.mergeable !== newMergeable) {
|
|
797
|
+
pr.mergeable = newMergeable;
|
|
798
|
+
updated = true;
|
|
799
|
+
}
|
|
800
|
+
const newIsDraft = prData.isDraft === true;
|
|
801
|
+
if (pr.isDraft !== newIsDraft) {
|
|
802
|
+
pr.isDraft = newIsDraft;
|
|
803
|
+
updated = true;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// P-w3a7b9c4 — Phase 1.3: ADO PRs always get pr.behindBy = null
|
|
807
|
+
// (GitHub-only feature for v1; see comment block above for the gap).
|
|
808
|
+
if (pr.behindBy !== null) {
|
|
809
|
+
pr.behindBy = null;
|
|
810
|
+
updated = true;
|
|
811
|
+
}
|
|
812
|
+
|
|
749
813
|
const reviewers = prData.reviewers || [];
|
|
750
814
|
const votes = reviewers.map(r => r.vote).filter(v => v !== undefined);
|
|
751
815
|
let newReviewStatus = pr.reviewStatus || 'pending';
|
|
@@ -1568,4 +1632,14 @@ module.exports = {
|
|
|
1568
1632
|
_resetAdoThrottle, // exported for testing
|
|
1569
1633
|
_setAdoThrottleForTest, // exported for testing
|
|
1570
1634
|
_setAdoTokenForTest, // exported for testing
|
|
1635
|
+
// Exported for unit tests — engine code MUST go through pollPrStatus / reconcilePrs.
|
|
1636
|
+
decodeUrlSegment,
|
|
1637
|
+
stripGitSuffix,
|
|
1638
|
+
encodeAdoPathSegment,
|
|
1639
|
+
buildAdoPrUrlBase,
|
|
1640
|
+
adoRepoCandidateFromParts,
|
|
1641
|
+
parseAdoRepoMetadata,
|
|
1642
|
+
parseCanonicalAdoPrId,
|
|
1643
|
+
isAdoRepairCandidateCompatible,
|
|
1644
|
+
getMissingAdoProjectConfigFields,
|
|
1571
1645
|
};
|