[glm-grade=A] fix(auto-restart): await async servicesStateManager.read() so handleContainerDown actually fires (DC-060)
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s

This commit is contained in:
Hermes
2026-08-18 05:52:32 -07:00
parent 87f76aef66
commit 71d20ceef3
2 changed files with 76 additions and 27 deletions
@@ -336,32 +336,77 @@ describe('AutoRestartManager', () => {
});
describe('_resolveContainerId', () => {
test('returns containerId from status.details when present', () => {
test('returns containerId from status.details when present', async () => {
const { manager } = makeManager();
const cid = manager._resolveContainerId('svc-1', { details: { containerId: 'cid-details' } });
const cid = await manager._resolveContainerId('svc-1', { details: { containerId: 'cid-details' } });
expect(cid).toBe('cid-details');
});
test('falls back to healthChecker.config.services[serviceId].containerId', () => {
test('falls back to healthChecker.config.services[serviceId].containerId', async () => {
const { manager, healthChecker } = makeManager();
healthChecker.config = { services: { 'svc-1': { containerId: 'cid-hc' } } };
const cid = manager._resolveContainerId('svc-1', { details: {} });
const cid = await manager._resolveContainerId('svc-1', { details: {} });
expect(cid).toBe('cid-hc');
});
test('falls back to servicesStateManager.read when sync list is returned', () => {
test('DC-060: awaits async servicesStateManager.read() and resolves containerId', async () => {
// Regression test for the auto-restart silently no-op bug:
// _resolveContainerId used to fire servicesStateManager.read() via
// .then(...) and discard the result. Callers gated on the return
// value, so a healthy→unhealthy transition whose only containerId
// source was the async state manager never triggered handleContainerDown.
const { manager, servicesStateManager } = makeManager();
servicesStateManager.read.mockReturnValue([
servicesStateManager.read.mockResolvedValue([
{ id: 'svc-1', containerId: 'cid-state' },
]);
const cid = manager._resolveContainerId('svc-1', { details: {} });
const cid = await manager._resolveContainerId('svc-1', { details: {} });
expect(cid).toBe('cid-state');
});
test('returns null when no source has a containerId', () => {
test('returns null when no source has a containerId', async () => {
const { manager } = makeManager();
const cid = manager._resolveContainerId('svc-unknown', { details: {} });
const cid = await manager._resolveContainerId('svc-unknown', { details: {} });
expect(cid).toBeNull();
});
test('swallows servicesStateManager.read() rejection', async () => {
const { manager, servicesStateManager } = makeManager();
servicesStateManager.read.mockRejectedValue(new Error('disk gone'));
const cid = await manager._resolveContainerId('svc-1', { details: {} });
expect(cid).toBeNull();
});
});
describe('DC-060: healthy→unhealthy transitions trigger restart via async lookup', () => {
test('handleContainerDown is invoked with containerId from async state-manager lookup', async () => {
// End-to-end: containerId comes ONLY from servicesStateManager.read()
// (the production path for services.json-backed deployments).
const { manager, docker, servicesStateManager } = makeManager();
docker.client.getContainer.mockReturnValue({
start: jest.fn().mockResolvedValue(undefined),
});
await manager.setPolicy('svc-1', { maxRetries: 3, retryIntervalMs: 0 });
manager._previousHealth.set('svc-1', 'up');
servicesStateManager.read.mockResolvedValue([
{ id: 'svc-1', containerId: 'cid-from-state' },
]);
const handleDownSpy = jest.spyOn(manager, 'handleContainerDown');
await manager._handleStatusCheck({ serviceId: 'svc-1', status: 'down' });
expect(handleDownSpy).toHaveBeenCalledWith('svc-1', 'cid-from-state');
});
test('handleContainerDown is NOT invoked when async lookup returns no containerId', async () => {
const { manager, servicesStateManager } = makeManager();
await manager.setPolicy('svc-1', { maxRetries: 3, retryIntervalMs: 0 });
manager._previousHealth.set('svc-1', 'up');
servicesStateManager.read.mockResolvedValue([
{ id: 'svc-1' /* no containerId */ },
]);
const handleDownSpy = jest.spyOn(manager, 'handleContainerDown');
await manager._handleStatusCheck({ serviceId: 'svc-1', status: 'down' });
expect(handleDownSpy).not.toHaveBeenCalled();
});
});
});