Skip to content

Commit be00e08

Browse files
add some tests for new stuff
1 parent 083f173 commit be00e08

7 files changed

Lines changed: 745 additions & 1 deletion

File tree

jest.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export default {
2121
"^@/(.*)$": "<rootDir>/src/$1",
2222
"^@open-api/(.*)$": "<rootDir>/src/open-api/$1",
2323
"^@test-utils/(.*)$": "<rootDir>/src/integration-tests/utils/$1",
24+
"^(.*)\\.js$": "$1",
2425
},
2526
transform: {
2627
"^.+\\.tsx?$": [

src/sdk/clients/worker/events/__tests__/EventDispatcher.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
TaskExecutionFailure,
1010
TaskUpdateCompleted,
1111
TaskUpdateFailure,
12+
TaskPaused,
1213
} from "../types";
1314

1415
describe("EventDispatcher", () => {
@@ -143,6 +144,7 @@ describe("EventDispatcher", () => {
143144
onTaskExecutionFailure: jest.fn<() => void>(),
144145
onTaskUpdateCompleted: jest.fn<() => void>(),
145146
onTaskUpdateFailure: jest.fn<() => void>(),
147+
onTaskPaused: jest.fn<() => void>(),
146148
};
147149

148150
dispatcher.register(listener);
@@ -231,6 +233,14 @@ describe("EventDispatcher", () => {
231233
};
232234
await dispatcher.publishTaskUpdateFailure(updateFailure);
233235
expect(listener.onTaskUpdateFailure).toHaveBeenCalledWith(updateFailure);
236+
237+
// Test TaskPaused
238+
const taskPaused: TaskPaused = {
239+
taskType: "test-task",
240+
timestamp: new Date(),
241+
};
242+
await dispatcher.publishTaskPaused(taskPaused);
243+
expect(listener.onTaskPaused).toHaveBeenCalledWith(taskPaused);
234244
});
235245

236246
test("should have zero overhead when no listeners registered", async () => {

src/sdk/createConductorClient/helpers/__tests__/fetchWithRetry.test.ts

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { jest, expect, describe, it, beforeEach } from "@jest/globals";
1+
import { jest, expect, describe, it, beforeEach, afterEach } from "@jest/globals";
22
import { retryFetch, wrapFetchWithRetry, applyTimeout } from "../fetchWithRetry";
3+
import * as httpObserver from "@/sdk/worker/metrics/httpObserver";
34

45
const createMockResponse = (status: number, body = ""): Response =>
56
new Response(body, { status, statusText: `Status ${status}` });
@@ -525,4 +526,107 @@ describe("fetchWithRetry", () => {
525526
expect(result.status).toBe(200);
526527
});
527528
});
529+
530+
// ─── wrapFetchWithRetry metrics recording ───────────────────────────
531+
532+
describe("wrapFetchWithRetry metrics", () => {
533+
const mockRecordApiRequestTime = jest.fn<
534+
(m: string, u: string, s: string, d: number) => void
535+
>();
536+
537+
beforeEach(() => {
538+
mockRecordApiRequestTime.mockClear();
539+
httpObserver.setHttpMetricsObserver({
540+
recordApiRequestTime: mockRecordApiRequestTime,
541+
recordWorkflowInputSize: jest.fn(),
542+
recordWorkflowStartError: jest.fn(),
543+
});
544+
});
545+
546+
afterEach(() => {
547+
httpObserver.setHttpMetricsObserver(undefined);
548+
});
549+
550+
it("should record API request time on successful response", async () => {
551+
mockFetch.mockResolvedValue(createMockResponse(200));
552+
553+
const wrappedFetch = wrapFetchWithRetry(mockFetch);
554+
await wrappedFetch("http://test.com/api/tasks", { method: "POST" });
555+
556+
expect(mockRecordApiRequestTime).toHaveBeenCalledTimes(1);
557+
const [method, uri, status, duration] =
558+
mockRecordApiRequestTime.mock.calls[0];
559+
expect(method).toBe("POST");
560+
expect(uri).toBe("/api/tasks");
561+
expect(status).toBe("200");
562+
expect(duration).toBeGreaterThanOrEqual(0);
563+
});
564+
565+
it("should record status '0' on network failure", async () => {
566+
mockFetch.mockRejectedValue(new Error("ECONNRESET"));
567+
568+
const wrappedFetch = wrapFetchWithRetry(mockFetch, {
569+
maxTransportRetries: 0,
570+
});
571+
572+
await expect(
573+
wrappedFetch("http://test.com/api/workflow")
574+
).rejects.toThrow("ECONNRESET");
575+
576+
expect(mockRecordApiRequestTime).toHaveBeenCalledTimes(1);
577+
const [method, uri, status] =
578+
mockRecordApiRequestTime.mock.calls[0];
579+
expect(method).toBe("GET");
580+
expect(uri).toBe("/api/workflow");
581+
expect(status).toBe("0");
582+
});
583+
584+
it("should extract method and URI from string URL", async () => {
585+
mockFetch.mockResolvedValue(createMockResponse(201));
586+
587+
const wrappedFetch = wrapFetchWithRetry(mockFetch);
588+
await wrappedFetch("http://example.com/tasks/123", { method: "PUT" });
589+
590+
expect(mockRecordApiRequestTime).toHaveBeenCalledTimes(1);
591+
const [method, uri] = mockRecordApiRequestTime.mock.calls[0];
592+
expect(method).toBe("PUT");
593+
expect(uri).toBe("/tasks/123");
594+
});
595+
596+
it("should extract method and URI from Request object", async () => {
597+
mockFetch.mockResolvedValue(createMockResponse(200));
598+
599+
const wrappedFetch = wrapFetchWithRetry(mockFetch);
600+
const request = new Request("http://example.com/api/metadata", {
601+
method: "DELETE",
602+
});
603+
await wrappedFetch(request);
604+
605+
expect(mockRecordApiRequestTime).toHaveBeenCalledTimes(1);
606+
const [method, uri] = mockRecordApiRequestTime.mock.calls[0];
607+
expect(method).toBe("DELETE");
608+
expect(uri).toBe("/api/metadata");
609+
});
610+
611+
it("should not break fetch when no observer is registered", async () => {
612+
httpObserver.setHttpMetricsObserver(undefined);
613+
614+
mockFetch.mockResolvedValue(createMockResponse(200));
615+
const wrappedFetch = wrapFetchWithRetry(mockFetch);
616+
const result = await wrappedFetch("http://test.com");
617+
618+
expect(result.status).toBe(200);
619+
});
620+
621+
it("should default method to GET when init has no method", async () => {
622+
mockFetch.mockResolvedValue(createMockResponse(200));
623+
624+
const wrappedFetch = wrapFetchWithRetry(mockFetch);
625+
await wrappedFetch("http://test.com/path");
626+
627+
expect(mockRecordApiRequestTime).toHaveBeenCalledTimes(1);
628+
const [method] = mockRecordApiRequestTime.mock.calls[0];
629+
expect(method).toBe("GET");
630+
});
631+
});
528632
});

src/sdk/worker/metrics/__tests__/CanonicalMetricsCollector.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,4 +318,116 @@ describe("CanonicalMetricsCollector", () => {
318318
await expect(collector.stop()).resolves.toBeUndefined();
319319
});
320320
});
321+
322+
describe("default argument handling", () => {
323+
it("recordUncaughtException with no arg should default to 'Error'", () => {
324+
collector.recordUncaughtException();
325+
const text = collector.toPrometheusText();
326+
expect(text).toContain('thread_uncaught_exceptions_total{exception="Error"} 1');
327+
});
328+
329+
it("recordWorkflowStartError with no args should default both labels", () => {
330+
collector.recordWorkflowStartError();
331+
const text = collector.toPrometheusText();
332+
expect(text).toContain('workflow_start_error_total{workflowType="",exception="Error"} 1');
333+
});
334+
335+
it("recordWorkflowStartError with only workflowType should default exception", () => {
336+
collector.recordWorkflowStartError("my_wf");
337+
const text = collector.toPrometheusText();
338+
expect(text).toContain('workflow_start_error_total{workflowType="my_wf",exception="Error"} 1');
339+
});
340+
341+
it("recordExternalPayloadUsed with missing optional args should default to empty strings", () => {
342+
collector.recordExternalPayloadUsed("TASK_OUTPUT");
343+
const text = collector.toPrometheusText();
344+
expect(text).toContain('external_payload_used_total{entityName="",operation="",payloadType="TASK_OUTPUT"} 1');
345+
});
346+
347+
it("recordTaskAckError with no exception arg should default to 'Error'", () => {
348+
collector.recordTaskAckError("task_a");
349+
const text = collector.toPrometheusText();
350+
expect(text).toContain('task_ack_error_total{taskType="task_a",exception="Error"} 1');
351+
});
352+
353+
it("recordWorkflowInputSize with no version should default to empty string", () => {
354+
collector.recordWorkflowInputSize("my_wf", 1000);
355+
const text = collector.toPrometheusText();
356+
expect(text).toContain('workflow_input_size_bytes_sum{workflowType="my_wf",version=""} 1000');
357+
});
358+
});
359+
360+
describe("execution completion without outputSizeBytes", () => {
361+
it("should not emit task_result_size_bytes when outputSizeBytes is undefined", () => {
362+
collector.onTaskExecutionCompleted({
363+
taskType: "task_a",
364+
taskId: "t1",
365+
workerId: "w1",
366+
durationMs: 200,
367+
timestamp: new Date(),
368+
});
369+
370+
const text = collector.toPrometheusText();
371+
expect(text).toContain("task_execute_time_seconds");
372+
expect(text).not.toContain("task_result_size_bytes");
373+
});
374+
});
375+
376+
describe("output helpers without prom-client", () => {
377+
it("getContentType should return plain text fallback", () => {
378+
expect(collector.getContentType()).toBe(
379+
"text/plain; version=0.0.4; charset=utf-8"
380+
);
381+
});
382+
383+
it("toPrometheusTextAsync should fall back to sync text", async () => {
384+
collector.onPollStarted({
385+
taskType: "t",
386+
workerId: "w",
387+
pollCount: 1,
388+
timestamp: new Date(),
389+
});
390+
const asyncText = await collector.toPrometheusTextAsync();
391+
const syncText = collector.toPrometheusText();
392+
expect(asyncText).toBe(syncText);
393+
});
394+
395+
it("toPrometheusText should ignore _prefix argument", () => {
396+
collector.onPollStarted({
397+
taskType: "t",
398+
workerId: "w",
399+
pollCount: 1,
400+
timestamp: new Date(),
401+
});
402+
const text = collector.toPrometheusText("some_prefix");
403+
expect(text).not.toContain("some_prefix");
404+
expect(text).toContain("task_poll_total{");
405+
});
406+
});
407+
408+
describe("active_workers gauge on failure", () => {
409+
it("should decrement active_workers on task execution failure", () => {
410+
collector.onTaskExecutionStarted({
411+
taskType: "task_a",
412+
taskId: "t1",
413+
workerId: "w1",
414+
timestamp: new Date(),
415+
});
416+
417+
let text = collector.toPrometheusText();
418+
expect(text).toContain('active_workers{taskType="task_a"} 1');
419+
420+
collector.onTaskExecutionFailure({
421+
taskType: "task_a",
422+
taskId: "t1",
423+
workerId: "w1",
424+
cause: new Error("fail"),
425+
durationMs: 100,
426+
timestamp: new Date(),
427+
});
428+
429+
text = collector.toPrometheusText();
430+
expect(text).toContain('active_workers{taskType="task_a"} 0');
431+
});
432+
});
321433
});

0 commit comments

Comments
 (0)