[grade=pending] QA sprint: commit 103 at-risk files from multi-agent sprint work

Committed by Hermes autonomous QA sprint 2026-08-13.
These files were modified during the Aug 12 sprint but never committed.
This commit is contained in:
Krystie
2026-08-12 17:34:10 -07:00
parent 0bf4406253
commit 503de258b8
105 changed files with 14057 additions and 2632 deletions
+47 -29
View File
@@ -8,6 +8,7 @@
const EventEmitter = require('events');
const fs = require('fs');
const path = require('path');
const { log } = require('../utils/logging');
const platformPaths = require('../../platform-paths');
const WORKFLOWS_FILE = process.env.WORKFLOWS_FILE || path.join(platformPaths.dataDir, 'workflows-config.json');
@@ -102,7 +103,7 @@ class WorkflowEngine extends EventEmitter {
this.enabled = new Map(Object.entries(data.enabled || {}));
}
} catch (error) {
console.error('[WorkflowEngine] Error loading config:', error.message);
log.error('workflow', error, { operation: 'loadConfig' });
}
// Default all workflows to enabled if not explicitly set
@@ -123,7 +124,7 @@ class WorkflowEngine extends EventEmitter {
};
fs.writeFileSync(WORKFLOWS_FILE, JSON.stringify(data, null, 2));
} catch (error) {
console.error('[WorkflowEngine] Error saving config:', error.message);
log.error('workflow', error, { operation: 'saveConfig' });
}
}
@@ -136,7 +137,7 @@ class WorkflowEngine extends EventEmitter {
this.history = JSON.parse(fs.readFileSync(WORKFLOW_HISTORY_FILE, 'utf8'));
}
} catch (error) {
console.error('[WorkflowEngine] Error loading history:', error.message);
log.error('workflow', error, { operation: 'loadHistory' });
this.history = [];
}
}
@@ -148,7 +149,7 @@ class WorkflowEngine extends EventEmitter {
try {
fs.writeFileSync(WORKFLOW_HISTORY_FILE, JSON.stringify(this.history, null, 2));
} catch (error) {
console.error('[WorkflowEngine] Error saving history:', error.message);
log.error('workflow', error, { operation: 'saveHistory' });
}
}
@@ -174,11 +175,11 @@ class WorkflowEngine extends EventEmitter {
const job = setInterval(() => {
this.executeWorkflow(workflowId, { trigger: 'scheduled', timestamp: new Date().toISOString() })
.catch(err => console.error(`[WorkflowEngine] Scheduled workflow ${workflowId} failed:`, err.message));
.catch(err => log.error('workflow', err, { workflowId, phase: 'scheduled' }));
}, workflow.interval);
this.scheduledJobs.set(workflowId, job);
console.log(`[WorkflowEngine] Scheduled workflow '${workflowId}' every ${workflow.interval}ms`);
log.info('workflow', 'Scheduled workflow', { workflowId, intervalMs: workflow.interval });
}
/**
@@ -201,14 +202,14 @@ class WorkflowEngine extends EventEmitter {
}
if (!this.enabled.get(workflowId)) {
console.log(`[WorkflowEngine] Workflow ${workflowId} is disabled, skipping`);
log.info('workflow', 'Workflow disabled, skipping', { workflowId });
return { skipped: true, reason: 'disabled' };
}
const executionId = `${workflowId}-${Date.now()}`;
const startTime = Date.now();
console.log(`[WorkflowEngine] Executing workflow: ${workflowId}`);
log.info('workflow', 'Executing workflow', { workflowId });
this.emit('workflow-start', { workflowId, executionId, triggerData });
const results = await this._runActions(workflow.actions, triggerData);
@@ -237,7 +238,7 @@ class WorkflowEngine extends EventEmitter {
this.saveHistory();
this.emit('workflow-complete', historyEntry);
console.log(`[WorkflowEngine] Workflow ${workflowId} completed in ${duration}ms, success: ${allSucceeded}`);
log.info('workflow', 'Workflow completed', { workflowId, durationMs: duration, success: allSucceeded });
return historyEntry;
}
@@ -251,32 +252,49 @@ class WorkflowEngine extends EventEmitter {
*/
async _runActions(actions, triggerData = {}) {
const results = [];
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 2000;
for (let i = 0; i < actions.length; i++) {
const action = actions[i];
const previousResult = i > 0 ? results[i - 1] : null;
// notify-on-failure needs to see the previous action's outcome to decide
// whether to fire. Passing the full results array in the trigger data lets
// executeAction do that lookup without changing the action shape.
// Also surface failingServices (set by healthCheckService on throw) so
// template variables like {{failingServices}} can interpolate.
const actionContext = {
...triggerData,
previousResult,
failingServices: previousResult && previousResult.failingServices ? previousResult.failingServices : undefined,
};
try {
const result = await this.executeAction(action, actionContext);
// DC-093: Retry with exponential backoff for transient failures
let lastError = null;
let result = null;
let succeeded = false;
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
try {
result = await this.executeAction(action, actionContext);
succeeded = true;
break;
} catch (error) {
lastError = error;
if (attempt < MAX_RETRIES) {
const delay = RETRY_DELAY_MS * Math.pow(2, attempt);
log.warn('workflow', `Action "${action.type}" failed (attempt ${attempt + 1}/${MAX_RETRIES + 1}), retrying in ${delay}ms`, { error: error.message });
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
if (succeeded) {
results.push({ action: action.type, success: true, result });
} catch (error) {
console.error(`[WorkflowEngine] Action ${action.type} failed:`, error.message);
} else {
log.error('workflow', `Action "${action.type}" failed after ${MAX_RETRIES + 1} attempts`, { error: lastError.message });
results.push({
action: action.type,
success: false,
error: error.message,
failingServices: error.failingServices,
error: lastError.message,
failingServices: lastError.failingServices,
exhaustedRetries: MAX_RETRIES + 1,
});
// Continue with other actions but log failure
}
}
@@ -322,7 +340,7 @@ class WorkflowEngine extends EventEmitter {
return this.collectMetrics(context.containerId, action.period);
default:
console.warn(`[WorkflowEngine] Unknown action type: ${action.type}`);
log.warn('workflow', 'Unknown action type', { actionType: action.type });
return { skipped: true, reason: `Unknown action type: ${action.type}` };
}
}
@@ -428,7 +446,7 @@ class WorkflowEngine extends EventEmitter {
throw new Error('Container ID not provided');
}
console.log(`[WorkflowEngine] Restarting container: ${containerId}`);
log.info('workflow', 'Restarting container', { containerId });
const container = docker.getContainer(containerId);
await container.restart();
@@ -448,7 +466,7 @@ class WorkflowEngine extends EventEmitter {
throw new Error('App ID not provided');
}
console.log(`[WorkflowEngine] Creating backup for: ${appId}`);
log.info('workflow', 'Creating backup', { appId });
// Use backup manager's executeBackup if available
const backupName = `${appId}-${label}`;
@@ -477,11 +495,11 @@ class WorkflowEngine extends EventEmitter {
async notify(message, channel) {
const notification = this.ctx.notification;
if (!notification) {
console.warn('[WorkflowEngine] Notification manager not available');
log.warn('workflow', 'Notification manager not available');
return { notified: false, reason: 'no notification manager' };
}
console.log(`[WorkflowEngine] Sending notification: ${message}`);
log.info('workflow', 'Sending notification', { message });
notification.send('workflow', 'Workflow Notification', message, 'info');
return { notified: true, message };
@@ -548,7 +566,7 @@ class WorkflowEngine extends EventEmitter {
}
}
console.log(`[WorkflowEngine] Workflow ${workflowId} ${enabled ? 'enabled' : 'disabled'}`);
log.info('workflow', 'Workflow toggled', { workflowId, enabled });
return { workflowId, enabled };
}
@@ -581,7 +599,7 @@ class WorkflowEngine extends EventEmitter {
const conditionMet = this.evaluateCondition(workflow.condition, eventData);
return conditionMet;
} catch (e) {
console.warn(`[WorkflowEngine] Condition evaluation failed for ${id}:`, e.message);
log.warn('workflow', 'Condition evaluation failed', { workflowId: id, error: e.message });
return false;
}
}
@@ -641,7 +659,7 @@ class WorkflowEngine extends EventEmitter {
for (const [workflowId] of this.scheduledJobs) {
this.stopScheduledWorkflow(workflowId);
}
console.log('[WorkflowEngine] All scheduled workflows stopped');
log.info('workflow', 'All scheduled workflows stopped');
}
}