fix(companion): validate runtime and heartbeat loop options

This commit is contained in:
William Valentin
2026-02-16 18:47:43 -08:00
parent 873dc1ad5b
commit a5c5a320ca
5 changed files with 52 additions and 3 deletions
+14
View File
@@ -368,6 +368,20 @@
],
"test_status": "pnpm test:run src/companion/heartbeatLoop.test.ts src/companion/runtimeClient.test.ts src/companion/platformClients.test.ts src/companion/platformClients.integration.test.ts + pnpm typecheck passing"
},
"companion-runtime-and-loop-option-validation": {
"status": "completed",
"date": "2026-02-17",
"updated": "2026-02-17",
"summary": "Added constructor option validation guards for companion runtime utilities (`requestTimeoutMs > 0`, `intervalMs > 0`, and valid `maxConsecutiveFailures` bounds) with regression tests.",
"files_modified": [
"src/companion/runtimeClient.ts",
"src/companion/runtimeClient.test.ts",
"src/companion/heartbeatLoop.ts",
"src/companion/heartbeatLoop.test.ts",
"docs/plans/state.json"
],
"test_status": "pnpm test:run src/companion/runtimeClient.test.ts src/companion/heartbeatLoop.test.ts src/companion/platformClients.test.ts src/companion/platformClients.integration.test.ts + pnpm typecheck passing"
},
"browser-tools-activation-clarity": {
"status": "completed",
"date": "2026-02-17",
+10
View File
@@ -37,6 +37,16 @@ describe('CompanionHeartbeatLoop', () => {
loop.stop();
});
it('validates constructor options', () => {
const publisher = { publishHeartbeat: vi.fn(async () => buildStatusResult()) };
expect(() => new CompanionHeartbeatLoop(publisher, { intervalMs: 0 })).toThrow(
'intervalMs must be a positive number',
);
expect(() => new CompanionHeartbeatLoop(publisher, { maxConsecutiveFailures: 0 })).toThrow(
'maxConsecutiveFailures must be >= 1 when specified',
);
});
it('supports delayed first run when runImmediately=false', async () => {
const publishHeartbeat = vi.fn(async () => buildStatusResult());
const loop = new CompanionHeartbeatLoop({ publishHeartbeat }, { intervalMs: 500 });
+14 -2
View File
@@ -31,11 +31,23 @@ export class CompanionHeartbeatLoop {
private consecutiveFailures = 0;
constructor(publisher: HeartbeatPublisher, options: CompanionHeartbeatLoopOptions = {}) {
const intervalMs = options.intervalMs ?? 30_000;
if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
throw new Error('intervalMs must be a positive number');
}
const maxConsecutiveFailures = options.maxConsecutiveFailures ?? Number.POSITIVE_INFINITY;
if (!Number.isFinite(maxConsecutiveFailures) && maxConsecutiveFailures !== Number.POSITIVE_INFINITY) {
throw new Error('maxConsecutiveFailures must be a positive number or Infinity');
}
if (Number.isFinite(maxConsecutiveFailures) && maxConsecutiveFailures < 1) {
throw new Error('maxConsecutiveFailures must be >= 1 when specified');
}
this.publisher = publisher;
this.intervalMs = options.intervalMs ?? 30_000;
this.intervalMs = intervalMs;
this.buildHeartbeat = options.buildHeartbeat;
this.onError = options.onError;
this.maxConsecutiveFailures = options.maxConsecutiveFailures ?? Number.POSITIVE_INFINITY;
this.maxConsecutiveFailures = maxConsecutiveFailures;
this.onFailureLimitReached = options.onFailureLimitReached;
}
+9
View File
@@ -96,6 +96,15 @@ afterAll(async () => {
});
describe('CompanionRuntimeClient', () => {
it('validates requestTimeoutMs option', () => {
expect(() => {
new CompanionRuntimeClient({
url: 'ws://127.0.0.1:1',
requestTimeoutMs: 0,
});
}).toThrow('requestTimeoutMs must be a positive number');
});
it('dispatches gateway events to subscribed handlers and supports unsubscribe', () => {
const client = new CompanionRuntimeClient({
url: 'ws://127.0.0.1:1',
+5 -1
View File
@@ -270,9 +270,13 @@ export class CompanionRuntimeClient {
private readonly eventHandlers = new Set<CompanionEventHandler>();
constructor(options: CompanionRuntimeClientOptions) {
const requestTimeoutMs = options.requestTimeoutMs ?? 15_000;
if (!Number.isFinite(requestTimeoutMs) || requestTimeoutMs <= 0) {
throw new Error('requestTimeoutMs must be a positive number');
}
this.url = options.url;
this.token = options.token;
this.requestTimeoutMs = options.requestTimeoutMs ?? 15_000;
this.requestTimeoutMs = requestTimeoutMs;
this.autoConnect = options.autoConnect ?? false;
this.websocketFactory = options.websocketFactory ?? ((url) => new WebSocket(url));
}