/** * Regression tests for WorkflowEngine.healthCheckService (DC-042 followup). * * Bug: bundled-workflows.js:310 called `servicesStateManager.getState()` — * a method that doesn't exist on StateManager. Combined with a missing * `await`, this returned a Promise instead of an array, which then short- * circuited via `|| []` to an empty array. The result: every health-check- * on-interval workflow ran successfully with 0 services checked, while * the workflow engine still reported "Action health-check failed: * servicesStateManager.getState is not a function" on the dashboard. * * Fix: call `await servicesStateManager.read()` with a .catch fallback to * an empty array so a corrupt/missing state file doesn't break the * workflow. */ const { WorkflowEngine } = require('../src/recipes/bundled-workflows'); function makeEngine(opts = {}) { const ctx = { servicesStateManager: opts.servicesStateManager || { read: jest.fn().mockResolvedValue([]), }, docker: opts.docker !== undefined ? opts.docker : { client: { getContainer: jest.fn(), }, }, }; const engine = new WorkflowEngine(ctx); // The constructor calls startScheduledWorkflows() which sets setInterval jobs. // Those prevent Jest from exiting cleanly. Clear them after construction. // We only care about healthCheckService behavior here, not scheduling. if (engine.scheduledJobs) { for (const job of engine.scheduledJobs.values()) { clearInterval(job); } engine.scheduledJobs.clear(); } return engine; } describe('WorkflowEngine.healthCheckService — DC-042 followup (getState bug)', () => { test('uses .read() not the non-existent .getState() — does not throw', async () => { const readMock = jest.fn().mockResolvedValue([]); const engine = makeEngine({ servicesStateManager: { read: readMock }, docker: undefined, // no docker — exercises the falsy branch }); // The original bug: this throws `servicesStateManager.getState is not a function` const result = await engine.healthCheckService('{{serviceId}}'); expect(readMock).toHaveBeenCalledTimes(1); expect(result).toEqual({ checked: 0, healthy: 0, results: [] }); }); test('returns checked/healthy counts from read() output', async () => { const docker = { client: { getContainer: jest.fn((id) => ({ inspect: jest.fn().mockResolvedValue({ State: { Running: id === 'c1' }, }), })), }, }; const engine = makeEngine({ servicesStateManager: { read: jest.fn().mockResolvedValue([ { id: 'svc-1', containerId: 'c1' }, { id: 'svc-2', containerId: 'c2' }, { id: 'svc-3' }, // no containerId, should be skipped ]), }, docker, }); const result = await engine.healthCheckService('{{serviceId}}'); expect(result.checked).toBe(2); // svc-3 skipped (no containerId) expect(result.healthy).toBe(1); // c1 is running, c2 is not expect(result.results).toHaveLength(2); expect(result.results[0]).toMatchObject({ service: 'svc-1', healthy: true }); expect(result.results[1]).toMatchObject({ service: 'svc-2', healthy: false }); }); test('gracefully degrades if read() throws — empty services list, no crash', async () => { const engine = makeEngine({ servicesStateManager: { read: jest.fn().mockRejectedValue(new Error('disk on fire')), }, docker: undefined, }); // Before the fix, this rejected because .read() wasn't called and the // .catch(() => []) fallback didn't exist. Now it should resolve to empty. const result = await engine.healthCheckService('{{serviceId}}'); expect(result).toEqual({ checked: 0, healthy: 0, results: [] }); }); test('servicesStateManager absent on ctx → no crash, empty result', async () => { const engine = new WorkflowEngine({ servicesStateManager: null, docker: undefined, }); // Same constructor cleanup if (engine.scheduledJobs) { for (const job of engine.scheduledJobs.values()) clearInterval(job); engine.scheduledJobs.clear(); } const result = await engine.healthCheckService('{{serviceId}}'); expect(result).toEqual({ checked: 0, healthy: 0, results: [] }); }); test('single service (non-template serviceId) path still works', async () => { const engine = makeEngine({ docker: { client: { getContainer: jest.fn(() => ({ inspect: jest.fn().mockResolvedValue({ State: { Running: true } }), })), }, }, }); const result = await engine.healthCheckService('single-svc-id'); expect(result).toEqual({ serviceId: 'single-svc-id', healthy: true }); }); });