|
1 | | -import { describe, expect, it } from "vitest"; |
| 1 | +import { beforeEach, describe, expect, it, vi } from "vitest"; |
2 | 2 | import { |
3 | 3 | finalizedSafeCutoff, |
| 4 | + LogsSearchProjector, |
4 | 5 | previewSafeCutoff, |
5 | 6 | selectFinalizedWindow, |
6 | 7 | selectPreviewWindow, |
| 8 | + type LogsSearchProjectorRedisStore, |
| 9 | + type LogsSearchProjectorStateStore, |
7 | 10 | } from "~/services/logsSearchProjector.server"; |
8 | 11 |
|
| 12 | +const telemetry = vi.hoisted(() => ({ |
| 13 | + recordWindow: vi.fn(), |
| 14 | + recordLeaseContention: vi.fn(), |
| 15 | + recordCheckpointConflict: vi.fn(), |
| 16 | + recordPreviewSkipped: vi.fn(), |
| 17 | + updateState: vi.fn(), |
| 18 | +})); |
| 19 | + |
| 20 | +vi.mock("~/services/logsSearchProjectorTelemetry.server", () => ({ |
| 21 | + logsSearchProjectorTelemetry: telemetry, |
| 22 | +})); |
| 23 | + |
9 | 24 | const at = (value: string) => new Date(value); |
10 | 25 |
|
| 26 | +beforeEach(() => { |
| 27 | + vi.clearAllMocks(); |
| 28 | +}); |
| 29 | + |
11 | 30 | describe("logs search projector window selection", () => { |
12 | 31 | it("floors preview work to a closed five-second boundary", () => { |
13 | 32 | expect(previewSafeCutoff(at("2026-08-14T12:10:09.999Z")).toISOString()).toBe( |
@@ -67,3 +86,59 @@ describe("logs search projector window selection", () => { |
67 | 86 | }); |
68 | 87 | }); |
69 | 88 | }); |
| 89 | + |
| 90 | +describe("logs search projector telemetry", () => { |
| 91 | + it("refreshes lag after a finalized projection failure", async () => { |
| 92 | + const now = at("2026-08-14T12:10:59.999Z"); |
| 93 | + const watermark = at("2026-08-14T12:05:00.000Z"); |
| 94 | + const control = { |
| 95 | + id: "task_events_search_v2", |
| 96 | + initialWatermark: watermark, |
| 97 | + paused: false, |
| 98 | + }; |
| 99 | + const stateStore = { |
| 100 | + initialize: vi.fn(async () => control), |
| 101 | + findControl: vi.fn(async () => control), |
| 102 | + getControl: vi.fn(async () => control), |
| 103 | + getFinalizedWatermark: vi.fn(async () => watermark), |
| 104 | + appendFinalizedCheckpoint: vi.fn(), |
| 105 | + pause: vi.fn(), |
| 106 | + resume: vi.fn(), |
| 107 | + } satisfies LogsSearchProjectorStateStore; |
| 108 | + const redisStore = { |
| 109 | + acquireLease: vi.fn(async () => true), |
| 110 | + releaseLease: vi.fn(), |
| 111 | + readLeaseStatus: vi.fn(async () => null), |
| 112 | + initializePreviewWatermark: vi.fn(async () => watermark), |
| 113 | + getPreviewWatermark: vi.fn(async () => null), |
| 114 | + advancePreviewWatermark: vi.fn(async () => true), |
| 115 | + } satisfies LogsSearchProjectorRedisStore; |
| 116 | + const projectionError = new Error("projection failed"); |
| 117 | + const projector = new LogsSearchProjector( |
| 118 | + { |
| 119 | + previewEnabled: true, |
| 120 | + maxFinalizedWindowsPerTick: 1, |
| 121 | + leaseDurationMs: 60_000, |
| 122 | + }, |
| 123 | + stateStore, |
| 124 | + redisStore, |
| 125 | + vi.fn(async () => { |
| 126 | + throw projectionError; |
| 127 | + }), |
| 128 | + () => now, |
| 129 | + { |
| 130 | + debug: vi.fn(), |
| 131 | + info: vi.fn(), |
| 132 | + warn: vi.fn(), |
| 133 | + error: vi.fn(), |
| 134 | + } |
| 135 | + ); |
| 136 | + |
| 137 | + await expect(projector.processTick()).rejects.toBe(projectionError); |
| 138 | + expect(telemetry.updateState).toHaveBeenCalledWith({ |
| 139 | + previewLagMs: null, |
| 140 | + finalizedLagMs: 180_000, |
| 141 | + paused: false, |
| 142 | + }); |
| 143 | + }); |
| 144 | +}); |
0 commit comments