fix(companion): validate runtime and heartbeat loop options
This commit is contained in:
@@ -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"
|
"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": {
|
"browser-tools-activation-clarity": {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"date": "2026-02-17",
|
"date": "2026-02-17",
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ describe('CompanionHeartbeatLoop', () => {
|
|||||||
loop.stop();
|
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 () => {
|
it('supports delayed first run when runImmediately=false', async () => {
|
||||||
const publishHeartbeat = vi.fn(async () => buildStatusResult());
|
const publishHeartbeat = vi.fn(async () => buildStatusResult());
|
||||||
const loop = new CompanionHeartbeatLoop({ publishHeartbeat }, { intervalMs: 500 });
|
const loop = new CompanionHeartbeatLoop({ publishHeartbeat }, { intervalMs: 500 });
|
||||||
|
|||||||
@@ -31,11 +31,23 @@ export class CompanionHeartbeatLoop {
|
|||||||
private consecutiveFailures = 0;
|
private consecutiveFailures = 0;
|
||||||
|
|
||||||
constructor(publisher: HeartbeatPublisher, options: CompanionHeartbeatLoopOptions = {}) {
|
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.publisher = publisher;
|
||||||
this.intervalMs = options.intervalMs ?? 30_000;
|
this.intervalMs = intervalMs;
|
||||||
this.buildHeartbeat = options.buildHeartbeat;
|
this.buildHeartbeat = options.buildHeartbeat;
|
||||||
this.onError = options.onError;
|
this.onError = options.onError;
|
||||||
this.maxConsecutiveFailures = options.maxConsecutiveFailures ?? Number.POSITIVE_INFINITY;
|
this.maxConsecutiveFailures = maxConsecutiveFailures;
|
||||||
this.onFailureLimitReached = options.onFailureLimitReached;
|
this.onFailureLimitReached = options.onFailureLimitReached;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,15 @@ afterAll(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('CompanionRuntimeClient', () => {
|
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', () => {
|
it('dispatches gateway events to subscribed handlers and supports unsubscribe', () => {
|
||||||
const client = new CompanionRuntimeClient({
|
const client = new CompanionRuntimeClient({
|
||||||
url: 'ws://127.0.0.1:1',
|
url: 'ws://127.0.0.1:1',
|
||||||
|
|||||||
@@ -270,9 +270,13 @@ export class CompanionRuntimeClient {
|
|||||||
private readonly eventHandlers = new Set<CompanionEventHandler>();
|
private readonly eventHandlers = new Set<CompanionEventHandler>();
|
||||||
|
|
||||||
constructor(options: CompanionRuntimeClientOptions) {
|
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.url = options.url;
|
||||||
this.token = options.token;
|
this.token = options.token;
|
||||||
this.requestTimeoutMs = options.requestTimeoutMs ?? 15_000;
|
this.requestTimeoutMs = requestTimeoutMs;
|
||||||
this.autoConnect = options.autoConnect ?? false;
|
this.autoConnect = options.autoConnect ?? false;
|
||||||
this.websocketFactory = options.websocketFactory ?? ((url) => new WebSocket(url));
|
this.websocketFactory = options.websocketFactory ?? ((url) => new WebSocket(url));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user