fix(mesh): log boot state to console so docker logs surfaces mesh status (#1159)

MeshService records its boot summary and setup failures only through
logActivity (in-memory ring buffer + WS listeners), which the Routing
tab consumes. The docker-logs surface was silent for the mesh
subsystem, so operators running Sencho-in-Docker had no boot-time
visibility into whether the data plane came up cleanly.

Mirror the existing logActivity entries to console without replacing
them:

- MeshService.start() success summary: console.log /
  console.warn / console.error gated on the summary level. Format:
  [Mesh] data plane ok, self attached at <ip>, subnet <X>
  [Mesh] data plane unavailable (<reason>: <message>)
- recordSetupFailure: console.warn for the expected dev-mode
  not_in_docker case, console.error for real failures. Format:
  [Mesh] data plane unavailable (<reason>, subnet <X>): <sanitized>

The activity entries that already drive the Routing tab banner stay
intact; the console lines are purely additive for the docker logs
workflow. Two new unit tests assert the failure and not_in_docker
console mirrors fire with the [Mesh] prefix.

Fixes F-5.
This commit is contained in:
Anso
2026-05-22 16:30:10 -04:00
committed by GitHub
parent c87dc7e747
commit 474290081d
3 changed files with 61 additions and 2 deletions
@@ -441,3 +441,43 @@ describe('MeshService.setupMeshNetwork subnet auto-fallback', () => {
expect(status.reason).toBe('subnet_mismatch');
});
});
describe('MeshService console.log mirror (F-5)', () => {
it('mirrors a setup failure to console.error with the [Mesh] prefix', async () => {
process.env.SENCHO_MESH_SUBNET = '10.42.0.0/24';
process.env.HOSTNAME = 'sencho';
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
mockDocker({
createNetwork: vi.fn().mockRejectedValue(
Object.assign(new Error('Pool overlaps with other one on this address space'), { statusCode: 500 }),
),
});
const svc = MeshService.getInstance();
await callSetup(svc);
const meshLines = errSpy.mock.calls
.map((args) => String(args[0]))
.filter((line) => line.startsWith('[Mesh] data plane unavailable'));
expect(meshLines.length).toBeGreaterThan(0);
expect(meshLines[0]).toContain('subnet_overlap');
expect(meshLines[0]).toContain('10.42.0.0/24');
expect(meshLines[0]).toMatch(/overlap/i);
});
it('mirrors a not_in_docker condition to console.warn (expected dev-mode case)', async () => {
process.env.SENCHO_MESH_SUBNET = '10.42.0.0/24';
delete process.env.HOSTNAME;
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
mockDocker();
const svc = MeshService.getInstance();
await callSetup(svc);
const meshLines = warnSpy.mock.calls
.map((args) => String(args[0]))
.filter((line) => line.startsWith('[Mesh] data plane unavailable'));
expect(meshLines.length).toBeGreaterThan(0);
expect(meshLines[0]).toContain('not_in_docker');
});
});