The bundled-workflows.js:310 call site used a non-existent .getState() method AND forgot to await. The Promise short-circuited via '|| []' to an empty array, so every health-check-on-interval workflow ran every 5 min reporting 'Action health-check failed: servicesStateManager.getState is not a function' while silently iterating over zero services. Visible on both DNS2 (production) and dc-contabo-de (test server) — same code, same bug, same log spam. Fix: 'await servicesStateManager.read().catch(() => []) || []' — uses the actual async method, returns empty array on read() failure (corrupt or missing state file shouldn't break the workflow), preserves the original short-circuit guard. New regression test __tests__/bundled-workflows-health-check.test.js with 5 cases: 1. uses .read() not the non-existent .getState() — does not throw 2. returns checked/healthy counts from read() output 3. gracefully degrades if read() throws — empty services list, no crash 4. servicesStateManager absent on ctx → no crash, empty result 5. single service (non-template serviceId) path still works Tests: 1219/1219 pass (1214 baseline + 5 new). ESLint: clean for the new file. Test fixture note: had to clearInterval the constructor's scheduledJobs so Jest could exit cleanly — scheduled workflows are not under test here.
132 lines
4.7 KiB
JavaScript
132 lines
4.7 KiB
JavaScript
/**
|
|
* 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 });
|
|
});
|
|
});
|