clawdie-ai/src/colibri-host-status.test.ts
Operator & Claude Code 2b611161a4 Add Colibri watchdog host-status seam
Adds src/colibri-host-status.ts and tests: connects to the watchdog IPC socket, requests status, and normalizes it into a structured Colibri host-status input. Additive and read-only (a new consumer alongside doctor.ts); the watchdog and its wire protocol are untouched, so doctor/pi-profile stay compatible (Colibri proof gate 4). No runner deletion.

Shape aligned with colibri-pi-run.ts and colibri-run-manifest.ts: string source discriminator, pure normalizer, Result union for fallible IO, and a summarizeColibriHostStatus text block matching summarizeColibriRunManifest.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---
Build: pass | Tests: FAIL — 2 failed
2026-05-24 21:28:03 +02:00

150 lines
3.8 KiB
TypeScript

import fs from 'fs';
import net from 'net';
import os from 'os';
import path from 'path';
import { afterEach, describe, expect, it } from 'vitest';
import {
normalizeWatchdogStatus,
readColibriHostStatus,
summarizeColibriHostStatus,
} from './colibri-host-status.js';
const OBSERVED_AT = '2026-05-24T12:00:00.000Z';
describe('colibri host-status normalization', () => {
it('normalizes a full watchdog status payload', () => {
const status = normalizeWatchdogStatus(
{
mode: 'auto',
throttled: false,
freeMemoryMB: 2048,
activeJails: 3,
queuedGroups: 1,
controlplane: { overallStatus: 'ok', checks: [], durationMs: 12 },
stripeStatus: 'live',
},
OBSERVED_AT,
);
expect(status).toMatchObject({
source: 'watchdog-socket',
observedAt: OBSERVED_AT,
mode: 'auto',
throttled: false,
freeMemoryMB: 2048,
activeJails: 3,
queuedGroups: 1,
controlplaneStatus: 'ok',
});
});
it('degrades missing or garbled fields to safe defaults', () => {
const status = normalizeWatchdogStatus({ mode: 123 }, OBSERVED_AT);
expect(status).toMatchObject({
mode: 'unknown',
throttled: false,
freeMemoryMB: 0,
activeJails: 0,
queuedGroups: 0,
controlplaneStatus: null,
});
});
it('renders a consistent text summary block', () => {
const summary = summarizeColibriHostStatus(
normalizeWatchdogStatus(
{
mode: 'auto',
throttled: true,
freeMemoryMB: 512,
activeJails: 2,
queuedGroups: 0,
controlplane: { overallStatus: 'degraded' },
},
OBSERVED_AT,
),
);
expect(summary).toContain('<colibri-host-status>');
expect(summary).toContain('mode=auto');
expect(summary).toContain('throttled=YES');
expect(summary).toContain('controlplane=degraded');
expect(summary).toContain('</colibri-host-status>');
});
});
describe('colibri host-status socket read', () => {
let server: net.Server | undefined;
const sockPath = path.join(
os.tmpdir(),
`colibri-hoststatus-test-${process.pid}.sock`,
);
afterEach(async () => {
if (server) {
await new Promise<void>((resolve) => server!.close(() => resolve()));
server = undefined;
}
try {
fs.unlinkSync(sockPath);
} catch {
/* ok if already gone */
}
});
it('reads and normalizes a watchdog {ok,data} response', async () => {
try {
fs.unlinkSync(sockPath);
} catch {
/* ok if absent */
}
server = net.createServer((socket) => {
socket.on('data', () => {
socket.write(
JSON.stringify({
ok: true,
data: {
mode: 'slow',
throttled: true,
freeMemoryMB: 256,
activeJails: 1,
queuedGroups: 0,
controlplane: null,
},
}) + '\n',
);
socket.end();
});
});
await new Promise<void>((resolve) => server!.listen(sockPath, resolve));
const result = await readColibriHostStatus({
socketPath: sockPath,
observedAt: OBSERVED_AT,
});
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.status).toMatchObject({
source: 'watchdog-socket',
mode: 'slow',
throttled: true,
freeMemoryMB: 256,
controlplaneStatus: null,
});
});
it('resolves to an error when the socket is unavailable', async () => {
const result = await readColibriHostStatus({
socketPath: path.join(os.tmpdir(), `colibri-absent-${process.pid}.sock`),
timeoutMs: 500,
});
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error).toMatch(/socket|timeout|ENOENT|ECONNREFUSED/i);
});
});