From d6e00c3dd4df9bdccfbad27289fac3d7d3b6de9b Mon Sep 17 00:00:00 2001
From: Michael Smith <throwawayclover@gmail.com>
Date: Fri, 23 May 2025 20:03:57 +0000
Subject: [PATCH 1/6] wip: commit progress on test update

---
 .../modules/resources/useAgentLogs.test.ts    | 117 ++++++++-----
 site/src/modules/resources/useAgentLogs.ts    |  90 ++++++----
 site/src/testHelpers/websockets.ts            | 152 +++++++++++++++++
 site/src/utils/OneWayWebSocket.test.ts        | 154 ++----------------
 4 files changed, 289 insertions(+), 224 deletions(-)
 create mode 100644 site/src/testHelpers/websockets.ts

diff --git a/site/src/modules/resources/useAgentLogs.test.ts b/site/src/modules/resources/useAgentLogs.test.ts
index a5339e00c87eb..72168ff6f16f1 100644
--- a/site/src/modules/resources/useAgentLogs.test.ts
+++ b/site/src/modules/resources/useAgentLogs.test.ts
@@ -1,60 +1,89 @@
-import { renderHook, waitFor } from "@testing-library/react";
+import {
+	renderHook,
+	type RenderHookResult,
+	waitFor,
+} from "@testing-library/react";
 import type { WorkspaceAgentLog } from "api/typesGenerated";
-import WS from "jest-websocket-mock";
 import { MockWorkspaceAgent } from "testHelpers/entities";
-import { useAgentLogs } from "./useAgentLogs";
+import { createUseAgentLogs } from "./useAgentLogs";
+import {
+	createMockWebSocket,
+	type MockWebSocketPublisher,
+} from "testHelpers/websockets";
+import { OneWayWebSocket } from "utils/OneWayWebSocket";
 
-/**
- * TODO: WS does not support multiple tests running at once in isolation so we
- * have one single test that test the most common scenario.
- * Issue: https://github.com/romgain/jest-websocket-mock/issues/172
- */
+function generateMockLogs(count: number): WorkspaceAgentLog[] {
+	return Array.from({ length: count }, (_, i) => ({
+		id: i,
+		created_at: new Date().toISOString(),
+		level: "info",
+		output: `Log ${i}`,
+		source_id: "",
+	}));
+}
 
-describe("useAgentLogs", () => {
-	afterEach(() => {
-		WS.clean();
+type MountHookResult = Readonly<
+	RenderHookResult<readonly WorkspaceAgentLog[], { enabled: boolean }> & {
+		publisher: MockWebSocketPublisher;
+	}
+>;
+
+function mountHook(): MountHookResult {
+	let publisher!: MockWebSocketPublisher;
+	const useAgentLogs = createUseAgentLogs((agentId, params) => {
+		return new OneWayWebSocket({
+			apiRoute: `/api/v2/workspaceagents/${agentId}/logs`,
+			searchParams: new URLSearchParams({
+				follow: "true",
+				after: params?.after?.toString() || "0",
+			}),
+			websocketInit: (url) => {
+				const [mockSocket, mockPub] = createMockWebSocket(url);
+				publisher = mockPub;
+				return mockSocket;
+			},
+		});
 	});
 
-	it("clear logs when disabled to avoid duplicates", async () => {
-		const server = new WS(
-			`ws://localhost/api/v2/workspaceagents/${
-				MockWorkspaceAgent.id
-			}/logs?follow&after=0`,
-		);
-		const { result, rerender } = renderHook(
-			({ enabled }) => useAgentLogs(MockWorkspaceAgent, enabled),
-			{ initialProps: { enabled: true } },
-		);
-		await server.connected;
-
-		// Send 3 logs
-		server.send(JSON.stringify(generateLogs(3)));
+	const { result, rerender, unmount } = renderHook(
+		({ enabled }) => useAgentLogs(MockWorkspaceAgent, enabled),
+		{ initialProps: { enabled: true } },
+	);
+
+	return { result, rerender, unmount, publisher };
+}
+
+describe("useAgentLogs", () => {
+	it("clears logs when hook becomes disabled (protection to avoid duplicate logs when hook goes back to being re-enabled)", async () => {
+		const { result, rerender, publisher } = mountHook();
+
+		// Verify that logs can be received after mount
+		const initialLogs = generateMockLogs(3);
+		const initialEvent = new MessageEvent<string>("message", {
+			data: JSON.stringify(initialLogs),
+		});
+		publisher.publishMessage(initialEvent);
 		await waitFor(() => {
-			expect(result.current).toHaveLength(3);
+			// Using expect.arrayContaining to account for the fact that we're
+			// not guaranteed to receive WebSocket events in order
+			expect(result.current).toEqual(expect.arrayContaining(initialLogs));
 		});
 
-		// Disable the hook
+		// Disable the hook (and have the hook close the connection behind the
+		// scenes)
 		rerender({ enabled: false });
-		await waitFor(() => {
-			expect(result.current).toHaveLength(0);
-		});
+		await waitFor(() => expect(result.current).toHaveLength(0));
 
-		// Enable the hook again
+		// Re-enable the hook (creating an entirely new connection), and send
+		// new logs
 		rerender({ enabled: true });
-		await server.connected;
-		server.send(JSON.stringify(generateLogs(3)));
+		const newLogs = generateMockLogs(3);
+		const newEvent = new MessageEvent<string>("message", {
+			data: JSON.stringify(newLogs),
+		});
+		publisher.publishMessage(newEvent);
 		await waitFor(() => {
-			expect(result.current).toHaveLength(3);
+			expect(result.current).toEqual(expect.arrayContaining(newLogs));
 		});
 	});
 });
-
-function generateLogs(count: number): WorkspaceAgentLog[] {
-	return Array.from({ length: count }, (_, i) => ({
-		id: i,
-		created_at: new Date().toISOString(),
-		level: "info",
-		output: `Log ${i}`,
-		source_id: "",
-	}));
-}
diff --git a/site/src/modules/resources/useAgentLogs.ts b/site/src/modules/resources/useAgentLogs.ts
index d7f810483a693..63ea4afbb99c1 100644
--- a/site/src/modules/resources/useAgentLogs.ts
+++ b/site/src/modules/resources/useAgentLogs.ts
@@ -3,45 +3,63 @@ import type { WorkspaceAgent, WorkspaceAgentLog } from "api/typesGenerated";
 import { displayError } from "components/GlobalSnackbar/utils";
 import { useEffect, useState } from "react";
 
-export function useAgentLogs(
-	agent: WorkspaceAgent,
-	enabled: boolean,
-): readonly WorkspaceAgentLog[] {
-	const [logs, setLogs] = useState<WorkspaceAgentLog[]>([]);
-
-	useEffect(() => {
-		if (!enabled) {
-			// Clean up the logs when the agent is not enabled. So it can receive logs
-			// from the beginning without duplicating the logs.
+export function createUseAgentLogs(
+	createSocket: typeof watchWorkspaceAgentLogs,
+) {
+	return function useAgentLogs(
+		agent: WorkspaceAgent,
+		enabled: boolean,
+	): readonly WorkspaceAgentLog[] {
+		const [logs, setLogs] = useState<readonly WorkspaceAgentLog[]>([]);
+
+		// Clean up the logs when the agent is not enabled, using a mid-render
+		// sync to remove any risk of screen flickering. Clearing the logs helps
+		// ensure that if the hook flips back to being enabled, we can receive a
+		// fresh set of logs from the beginning with zero risk of duplicates.
+		const [prevEnabled, setPrevEnabled] = useState(enabled);
+		if (!enabled && prevEnabled) {
 			setLogs([]);
-			return;
+			setPrevEnabled(false);
 		}
 
-		// Always fetch the logs from the beginning. We may want to optimize this in
-		// the future, but it would add some complexity in the code that maybe does
-		// not worth it.
-		const socket = watchWorkspaceAgentLogs(agent.id, { after: 0 });
-		socket.addEventListener("message", (e) => {
-			if (e.parseError) {
-				console.warn("Error parsing agent log: ", e.parseError);
+		useEffect(() => {
+			if (!enabled) {
 				return;
 			}
-			setLogs((logs) => [...logs, ...e.parsedMessage]);
-		});
-
-		socket.addEventListener("error", (e) => {
-			console.error("Error in agent log socket: ", e);
-			displayError(
-				"Unable to watch the agent logs",
-				"Please try refreshing the browser",
-			);
-			socket.close();
-		});
-
-		return () => {
-			socket.close();
-		};
-	}, [agent.id, enabled]);
-
-	return logs;
+
+			// Always fetch the logs from the beginning. We may want to optimize
+			// this in the future, but it would add some complexity in the code
+			// that might not be worth it.
+			const socket = createSocket(agent.id, { after: 0 });
+			socket.addEventListener("message", (e) => {
+				if (e.parseError) {
+					console.warn("Error parsing agent log: ", e.parseError);
+					return;
+				}
+				setLogs((logs) => [...logs, ...e.parsedMessage]);
+			});
+
+			socket.addEventListener("error", (e) => {
+				console.error("Error in agent log socket: ", e);
+				displayError(
+					"Unable to watch the agent logs",
+					"Please try refreshing the browser",
+				);
+				socket.close();
+			});
+
+			return () => socket.close();
+
+			// createSocket shouldn't ever change for the lifecycle of the hook,
+			// but Biome isn't smart enough to detect constant dependencies for
+			// higher-order hooks. Adding it to the array (even though it
+			// shouldn't ever be needed) seemed like the least fragile way to
+			// resolve the warning.
+		}, [createSocket, agent.id, enabled]);
+
+		return logs;
+	};
 }
+
+// The baseline implementation to use for production
+export const useAgentLogs = createUseAgentLogs(watchWorkspaceAgentLogs);
diff --git a/site/src/testHelpers/websockets.ts b/site/src/testHelpers/websockets.ts
new file mode 100644
index 0000000000000..3d98f543d0684
--- /dev/null
+++ b/site/src/testHelpers/websockets.ts
@@ -0,0 +1,152 @@
+import type { WebSocketEventType } from "utils/OneWayWebSocket";
+
+export type MockWebSocketPublisher = Readonly<{
+	publishMessage: (event: MessageEvent<string>) => void;
+	publishError: (event: ErrorEvent) => void;
+	publishClose: (event: CloseEvent) => void;
+	publishOpen: (event: Event) => void;
+}>;
+
+export type CreateMockWebSocketOptions = Readonly<{
+	// The URL to use to initialize the mock socket. This should match the
+	// "real" URL that you would pass to the built-in WebSocket constructor.
+	url: string;
+
+	// The additional WebSocket protocols to use when initializing. This should
+	// match the real protocols that you would pass to the built-in WebSocket
+	// constructor.
+	protocols?: string | string[];
+
+	// Indicates whether the mock socket should stay open after calling the
+	// .close method, so that it can be reused for a new connection. Defaults to
+	// false (meaning that the socket becomes completely unusable the first time
+	// after .close is called).
+	persistAfterClose?: boolean;
+}>;
+
+export function createMockWebSocket(
+	url: string,
+	protocols?: string | string[],
+): readonly [WebSocket, MockWebSocketPublisher] {
+	type EventMap = {
+		message: MessageEvent<string>;
+		error: ErrorEvent;
+		close: CloseEvent;
+		open: Event;
+	};
+	type CallbackStore = {
+		[K in keyof EventMap]: ((event: EventMap[K]) => void)[];
+	};
+
+	let activeProtocol: string;
+	if (Array.isArray(protocols)) {
+		activeProtocol = protocols[0] ?? "";
+	} else if (typeof protocols === "string") {
+		activeProtocol = protocols;
+	} else {
+		activeProtocol = "";
+	}
+
+	let closed = false;
+	const store: CallbackStore = {
+		message: [],
+		error: [],
+		close: [],
+		open: [],
+	};
+
+	const mockSocket: WebSocket = {
+		CONNECTING: 0,
+		OPEN: 1,
+		CLOSING: 2,
+		CLOSED: 3,
+
+		url,
+		protocol: activeProtocol,
+		readyState: 1,
+		binaryType: "blob",
+		bufferedAmount: 0,
+		extensions: "",
+		onclose: null,
+		onerror: null,
+		onmessage: null,
+		onopen: null,
+		send: jest.fn(),
+		dispatchEvent: jest.fn(),
+
+		addEventListener: <E extends WebSocketEventType>(
+			eventType: E,
+			callback: WebSocketEventMap[E],
+		) => {
+			if (closed) {
+				return;
+			}
+
+			const subscribers = store[eventType];
+			const cb = callback as unknown as CallbackStore[E][0];
+			if (!subscribers.includes(cb)) {
+				subscribers.push(cb);
+			}
+		},
+
+		removeEventListener: <E extends WebSocketEventType>(
+			eventType: E,
+			callback: WebSocketEventMap[E],
+		) => {
+			if (closed) {
+				return;
+			}
+
+			const subscribers = store[eventType];
+			const cb = callback as unknown as CallbackStore[E][0];
+			if (subscribers.includes(cb)) {
+				const updated = store[eventType].filter((c) => c !== cb);
+				store[eventType] = updated as unknown as CallbackStore[E];
+			}
+		},
+
+		close: () => {
+			closed = true;
+		},
+	};
+
+	const publisher: MockWebSocketPublisher = {
+		publishOpen: (event) => {
+			if (closed) {
+				return;
+			}
+			for (const sub of store.open) {
+				sub(event);
+			}
+		},
+
+		publishError: (event) => {
+			if (closed) {
+				return;
+			}
+			for (const sub of store.error) {
+				sub(event);
+			}
+		},
+
+		publishMessage: (event) => {
+			if (closed) {
+				return;
+			}
+			for (const sub of store.message) {
+				sub(event);
+			}
+		},
+
+		publishClose: (event) => {
+			if (closed) {
+				return;
+			}
+			for (const sub of store.close) {
+				sub(event);
+			}
+		},
+	};
+
+	return [mockSocket, publisher] as const;
+}
diff --git a/site/src/utils/OneWayWebSocket.test.ts b/site/src/utils/OneWayWebSocket.test.ts
index c6b00b593111f..65ab9cb084b8b 100644
--- a/site/src/utils/OneWayWebSocket.test.ts
+++ b/site/src/utils/OneWayWebSocket.test.ts
@@ -8,144 +8,10 @@
  */
 
 import {
-	type OneWayMessageEvent,
-	OneWayWebSocket,
-	type WebSocketEventType,
-} from "./OneWayWebSocket";
-
-type MockPublisher = Readonly<{
-	publishMessage: (event: MessageEvent<string>) => void;
-	publishError: (event: ErrorEvent) => void;
-	publishClose: (event: CloseEvent) => void;
-	publishOpen: (event: Event) => void;
-}>;
-
-function createMockWebSocket(
-	url: string,
-	protocols?: string | string[],
-): readonly [WebSocket, MockPublisher] {
-	type EventMap = {
-		message: MessageEvent<string>;
-		error: ErrorEvent;
-		close: CloseEvent;
-		open: Event;
-	};
-	type CallbackStore = {
-		[K in keyof EventMap]: ((event: EventMap[K]) => void)[];
-	};
-
-	let activeProtocol: string;
-	if (Array.isArray(protocols)) {
-		activeProtocol = protocols[0] ?? "";
-	} else if (typeof protocols === "string") {
-		activeProtocol = protocols;
-	} else {
-		activeProtocol = "";
-	}
-
-	let closed = false;
-	const store: CallbackStore = {
-		message: [],
-		error: [],
-		close: [],
-		open: [],
-	};
-
-	const mockSocket: WebSocket = {
-		CONNECTING: 0,
-		OPEN: 1,
-		CLOSING: 2,
-		CLOSED: 3,
-
-		url,
-		protocol: activeProtocol,
-		readyState: 1,
-		binaryType: "blob",
-		bufferedAmount: 0,
-		extensions: "",
-		onclose: null,
-		onerror: null,
-		onmessage: null,
-		onopen: null,
-		send: jest.fn(),
-		dispatchEvent: jest.fn(),
-
-		addEventListener: <E extends WebSocketEventType>(
-			eventType: E,
-			callback: WebSocketEventMap[E],
-		) => {
-			if (closed) {
-				return;
-			}
-
-			const subscribers = store[eventType];
-			const cb = callback as unknown as CallbackStore[E][0];
-			if (!subscribers.includes(cb)) {
-				subscribers.push(cb);
-			}
-		},
-
-		removeEventListener: <E extends WebSocketEventType>(
-			eventType: E,
-			callback: WebSocketEventMap[E],
-		) => {
-			if (closed) {
-				return;
-			}
-
-			const subscribers = store[eventType];
-			const cb = callback as unknown as CallbackStore[E][0];
-			if (subscribers.includes(cb)) {
-				const updated = store[eventType].filter((c) => c !== cb);
-				store[eventType] = updated as unknown as CallbackStore[E];
-			}
-		},
-
-		close: () => {
-			closed = true;
-		},
-	};
-
-	const publisher: MockPublisher = {
-		publishOpen: (event) => {
-			if (closed) {
-				return;
-			}
-			for (const sub of store.open) {
-				sub(event);
-			}
-		},
-
-		publishError: (event) => {
-			if (closed) {
-				return;
-			}
-			for (const sub of store.error) {
-				sub(event);
-			}
-		},
-
-		publishMessage: (event) => {
-			if (closed) {
-				return;
-			}
-			for (const sub of store.message) {
-				sub(event);
-			}
-		},
-
-		publishClose: (event) => {
-			if (closed) {
-				return;
-			}
-			for (const sub of store.close) {
-				sub(event);
-			}
-		},
-	};
-
-	return [mockSocket, publisher] as const;
-}
+	createMockWebSocket,
+	type MockWebSocketPublisher,
+} from "testHelpers/websockets";
+import { type OneWayMessageEvent, OneWayWebSocket } from "./OneWayWebSocket";
 
 describe(OneWayWebSocket.name, () => {
 	const dummyRoute = "/api/v2/blah";
@@ -167,7 +33,7 @@ describe(OneWayWebSocket.name, () => {
 	});
 
 	it("Lets a consumer add an event listener of each type", () => {
-		let publisher!: MockPublisher;
+		let publisher!: MockWebSocketPublisher;
 		const oneWay = new OneWayWebSocket({
 			apiRoute: dummyRoute,
 			websocketInit: (url, protocols) => {
@@ -207,7 +73,7 @@ describe(OneWayWebSocket.name, () => {
 	});
 
 	it("Lets a consumer remove an event listener of each type", () => {
-		let publisher!: MockPublisher;
+		let publisher!: MockWebSocketPublisher;
 		const oneWay = new OneWayWebSocket({
 			apiRoute: dummyRoute,
 			websocketInit: (url, protocols) => {
@@ -252,7 +118,7 @@ describe(OneWayWebSocket.name, () => {
 	});
 
 	it("Only calls each callback once if callback is added multiple times", () => {
-		let publisher!: MockPublisher;
+		let publisher!: MockWebSocketPublisher;
 		const oneWay = new OneWayWebSocket({
 			apiRoute: dummyRoute,
 			websocketInit: (url, protocols) => {
@@ -294,7 +160,7 @@ describe(OneWayWebSocket.name, () => {
 	});
 
 	it("Lets consumers register multiple callbacks for each event type", () => {
-		let publisher!: MockPublisher;
+		let publisher!: MockWebSocketPublisher;
 		const oneWay = new OneWayWebSocket({
 			apiRoute: dummyRoute,
 			websocketInit: (url, protocols) => {
@@ -375,7 +241,7 @@ describe(OneWayWebSocket.name, () => {
 	});
 
 	it("Gives consumers pre-parsed versions of message events", () => {
-		let publisher!: MockPublisher;
+		let publisher!: MockWebSocketPublisher;
 		const oneWay = new OneWayWebSocket({
 			apiRoute: dummyRoute,
 			websocketInit: (url, protocols) => {
@@ -405,7 +271,7 @@ describe(OneWayWebSocket.name, () => {
 	});
 
 	it("Exposes parsing error if message payload could not be parsed as JSON", () => {
-		let publisher!: MockPublisher;
+		let publisher!: MockWebSocketPublisher;
 		const oneWay = new OneWayWebSocket({
 			apiRoute: dummyRoute,
 			websocketInit: (url, protocols) => {

From 43d0ca8ec095b4cfe21aa50bbd49b886aa747e6d Mon Sep 17 00:00:00 2001
From: Michael Smith <throwawayclover@gmail.com>
Date: Fri, 23 May 2025 20:25:25 +0000
Subject: [PATCH 2/6] refactor: update useAgentLogs tests as unit tests

---
 .../modules/resources/useAgentLogs.test.ts    | 56 ++++++++++++-------
 1 file changed, 35 insertions(+), 21 deletions(-)

diff --git a/site/src/modules/resources/useAgentLogs.test.ts b/site/src/modules/resources/useAgentLogs.test.ts
index 72168ff6f16f1..085496486dbcf 100644
--- a/site/src/modules/resources/useAgentLogs.test.ts
+++ b/site/src/modules/resources/useAgentLogs.test.ts
@@ -1,8 +1,4 @@
-import {
-	renderHook,
-	type RenderHookResult,
-	waitFor,
-} from "@testing-library/react";
+import { renderHook, waitFor } from "@testing-library/react";
 import type { WorkspaceAgentLog } from "api/typesGenerated";
 import { MockWorkspaceAgent } from "testHelpers/entities";
 import { createUseAgentLogs } from "./useAgentLogs";
@@ -22,14 +18,28 @@ function generateMockLogs(count: number): WorkspaceAgentLog[] {
 	}));
 }
 
-type MountHookResult = Readonly<
-	RenderHookResult<readonly WorkspaceAgentLog[], { enabled: boolean }> & {
-		publisher: MockWebSocketPublisher;
-	}
->;
+// A mutable object holding the most recent mock WebSocket connection. This
+// value will change as the hook opens/closes new connections
+type PublisherResult = {
+	current: MockWebSocketPublisher;
+};
+
+type MountHookResult = Readonly<{
+	// Note: the value of `current` should be readonly, but the `current`
+	// property itself should be mutable
+	hookResult: {
+		current: readonly WorkspaceAgentLog[];
+	};
+	rerender: (props: { enabled: boolean }) => void;
+	publisherResult: PublisherResult;
+}>;
 
 function mountHook(): MountHookResult {
-	let publisher!: MockWebSocketPublisher;
+	// Have to cheat the types a little bit to avoid a chicken-and-the-egg
+	// scenario. publisherResult will be initialized with an undefined current
+	// value, but it'll be guaranteed not to be undefined by the time this
+	// function returns.
+	const publisherResult: Partial<PublisherResult> = { current: undefined };
 	const useAgentLogs = createUseAgentLogs((agentId, params) => {
 		return new OneWayWebSocket({
 			apiRoute: `/api/v2/workspaceagents/${agentId}/logs`,
@@ -38,41 +48,45 @@ function mountHook(): MountHookResult {
 				after: params?.after?.toString() || "0",
 			}),
 			websocketInit: (url) => {
-				const [mockSocket, mockPub] = createMockWebSocket(url);
-				publisher = mockPub;
+				const [mockSocket, mockPublisher] = createMockWebSocket(url);
+				publisherResult.current = mockPublisher;
 				return mockSocket;
 			},
 		});
 	});
 
-	const { result, rerender, unmount } = renderHook(
+	const { result, rerender } = renderHook(
 		({ enabled }) => useAgentLogs(MockWorkspaceAgent, enabled),
 		{ initialProps: { enabled: true } },
 	);
 
-	return { result, rerender, unmount, publisher };
+	return {
+		rerender,
+		hookResult: result,
+		publisherResult: publisherResult as PublisherResult,
+	};
 }
 
 describe("useAgentLogs", () => {
 	it("clears logs when hook becomes disabled (protection to avoid duplicate logs when hook goes back to being re-enabled)", async () => {
-		const { result, rerender, publisher } = mountHook();
+		const { hookResult, publisherResult, rerender } = mountHook();
 
 		// Verify that logs can be received after mount
 		const initialLogs = generateMockLogs(3);
 		const initialEvent = new MessageEvent<string>("message", {
 			data: JSON.stringify(initialLogs),
 		});
-		publisher.publishMessage(initialEvent);
+		publisherResult.current.publishMessage(initialEvent);
 		await waitFor(() => {
 			// Using expect.arrayContaining to account for the fact that we're
 			// not guaranteed to receive WebSocket events in order
-			expect(result.current).toEqual(expect.arrayContaining(initialLogs));
+			expect(hookResult.current).toEqual(expect.arrayContaining(initialLogs));
 		});
 
 		// Disable the hook (and have the hook close the connection behind the
 		// scenes)
 		rerender({ enabled: false });
-		await waitFor(() => expect(result.current).toHaveLength(0));
+		await waitFor(() => expect(hookResult.current).toHaveLength(0));
 
 		// Re-enable the hook (creating an entirely new connection), and send
 		// new logs
@@ -81,9 +95,9 @@ describe("useAgentLogs", () => {
 		const newEvent = new MessageEvent<string>("message", {
 			data: JSON.stringify(newLogs),
 		});
-		publisher.publishMessage(newEvent);
+		publisherResult.current.publishMessage(newEvent);
 		await waitFor(() => {
-			expect(result.current).toEqual(expect.arrayContaining(newLogs));
+			expect(hookResult.current).toEqual(expect.arrayContaining(newLogs));
 		});
 	});
 });

From 727bdddaa30c38a83809022acd3de393962d09c2 Mon Sep 17 00:00:00 2001
From: Michael Smith <throwawayclover@gmail.com>
Date: Fri, 23 May 2025 20:30:10 +0000
Subject: [PATCH 3/6] docs: rewrite comment for clarity

---
 site/src/modules/resources/useAgentLogs.test.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/site/src/modules/resources/useAgentLogs.test.ts b/site/src/modules/resources/useAgentLogs.test.ts
index 085496486dbcf..54161bf35ce7e 100644
--- a/site/src/modules/resources/useAgentLogs.test.ts
+++ b/site/src/modules/resources/useAgentLogs.test.ts
@@ -18,7 +18,7 @@ function generateMockLogs(count: number): WorkspaceAgentLog[] {
 	}));
 }
 
-// A mutable object holding the most recent mock WebSocket connection. This
+// A mutable object holding the most recent mock WebSocket publisher. The inner
 // value will change as the hook opens/closes new connections
 type PublisherResult = {
 	current: MockWebSocketPublisher;

From d46d1444ef0e54e599f721a1273ddc54d2a046ec Mon Sep 17 00:00:00 2001
From: Michael Smith <throwawayclover@gmail.com>
Date: Fri, 23 May 2025 20:31:23 +0000
Subject: [PATCH 4/6] fix: remove unnecessary type

---
 site/src/testHelpers/websockets.ts | 17 -----------------
 1 file changed, 17 deletions(-)

diff --git a/site/src/testHelpers/websockets.ts b/site/src/testHelpers/websockets.ts
index 3d98f543d0684..ac356f1c8dc28 100644
--- a/site/src/testHelpers/websockets.ts
+++ b/site/src/testHelpers/websockets.ts
@@ -7,23 +7,6 @@ export type MockWebSocketPublisher = Readonly<{
 	publishOpen: (event: Event) => void;
 }>;
 
-export type CreateMockWebSocketOptions = Readonly<{
-	// The URL to use to initialize the mock socket. This should match the
-	// "real" URL that you would pass to the built-in WebSocket constructor.
-	url: string;
-
-	// The additional WebSocket protocols to use when initializing. This should
-	// match the real protocols that you would pass to the built-in WebSocket
-	// constructor.
-	protocols?: string | string[];
-
-	// Indicates whether the mock socket should stay open after calling the
-	// .close method, so that it can be reused for a new connection. Defaults to
-	// false (meaning that the socket becomes completely unusable the first time
-	// after .close is called).
-	persistAfterClose?: boolean;
-}>;
-
 export function createMockWebSocket(
 	url: string,
 	protocols?: string | string[],

From 91a6fc12d304bb0aac59c48535fee964fd8ad3f0 Mon Sep 17 00:00:00 2001
From: Michael Smith <throwawayclover@gmail.com>
Date: Fri, 23 May 2025 21:04:13 +0000
Subject: [PATCH 5/6] fix: make sure logs have different timestamps

---
 .../modules/resources/useAgentLogs.test.ts    | 31 +++++++++++++------
 site/src/utils/OneWayWebSocket.test.ts        |  2 +-
 2 files changed, 22 insertions(+), 11 deletions(-)

diff --git a/site/src/modules/resources/useAgentLogs.test.ts b/site/src/modules/resources/useAgentLogs.test.ts
index 54161bf35ce7e..eedc7c4ad6393 100644
--- a/site/src/modules/resources/useAgentLogs.test.ts
+++ b/site/src/modules/resources/useAgentLogs.test.ts
@@ -1,21 +1,32 @@
 import { renderHook, waitFor } from "@testing-library/react";
 import type { WorkspaceAgentLog } from "api/typesGenerated";
 import { MockWorkspaceAgent } from "testHelpers/entities";
-import { createUseAgentLogs } from "./useAgentLogs";
 import {
-	createMockWebSocket,
 	type MockWebSocketPublisher,
+	createMockWebSocket,
 } from "testHelpers/websockets";
 import { OneWayWebSocket } from "utils/OneWayWebSocket";
+import { createUseAgentLogs } from "./useAgentLogs";
+
+const millisecondsInOneMinute = 60_000;
 
-function generateMockLogs(count: number): WorkspaceAgentLog[] {
-	return Array.from({ length: count }, (_, i) => ({
-		id: i,
-		created_at: new Date().toISOString(),
-		level: "info",
-		output: `Log ${i}`,
-		source_id: "",
-	}));
+function generateMockLogs(
+	logCount: number,
+	baseDate = new Date(),
+): readonly WorkspaceAgentLog[] {
+	return Array.from({ length: logCount }, (_, i) => {
+		// Make sure that the logs generated each have unique timestamps, so
+		// that we can test whether they're being sorted properly before being
+		// returned by the hook
+		const logDate = new Date(baseDate.getTime() + i * millisecondsInOneMinute);
+		return {
+			id: i,
+			created_at: logDate.toISOString(),
+			level: "info",
+			output: `Log ${i}`,
+			source_id: "",
+		};
+	});
 }
 
 // A mutable object holding the most recent mock WebSocket publisher. The inner
diff --git a/site/src/utils/OneWayWebSocket.test.ts b/site/src/utils/OneWayWebSocket.test.ts
index 65ab9cb084b8b..b334732c5b5e8 100644
--- a/site/src/utils/OneWayWebSocket.test.ts
+++ b/site/src/utils/OneWayWebSocket.test.ts
@@ -8,8 +8,8 @@
  */
 
 import {
-	createMockWebSocket,
 	type MockWebSocketPublisher,
+	createMockWebSocket,
 } from "testHelpers/websockets";
 import { type OneWayMessageEvent, OneWayWebSocket } from "./OneWayWebSocket";
 

From ecbe7b0466c01cf692a4ffcf73edcf43c1153dfa Mon Sep 17 00:00:00 2001
From: Michael Smith <throwawayclover@gmail.com>
Date: Fri, 23 May 2025 21:06:42 +0000
Subject: [PATCH 6/6] fix: add different dates to reduce risk of false
 positives

---
 site/src/modules/resources/useAgentLogs.test.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/site/src/modules/resources/useAgentLogs.test.ts b/site/src/modules/resources/useAgentLogs.test.ts
index eedc7c4ad6393..85206171bf7d5 100644
--- a/site/src/modules/resources/useAgentLogs.test.ts
+++ b/site/src/modules/resources/useAgentLogs.test.ts
@@ -83,7 +83,7 @@ describe("useAgentLogs", () => {
 		const { hookResult, publisherResult, rerender } = mountHook();
 
 		// Verify that logs can be received after mount
-		const initialLogs = generateMockLogs(3);
+		const initialLogs = generateMockLogs(3, new Date("april 5, 1997"));
 		const initialEvent = new MessageEvent<string>("message", {
 			data: JSON.stringify(initialLogs),
 		});
@@ -102,7 +102,7 @@ describe("useAgentLogs", () => {
 		// Re-enable the hook (creating an entirely new connection), and send
 		// new logs
 		rerender({ enabled: true });
-		const newLogs = generateMockLogs(3);
+		const newLogs = generateMockLogs(3, new Date("october 3, 2005"));
 		const newEvent = new MessageEvent<string>("message", {
 			data: JSON.stringify(newLogs),
 		});