From 34903f4e61a4a7750d3d3bd610670db40a117dba Mon Sep 17 00:00:00 2001 From: Rishavraaj Date: Tue, 3 Mar 2026 12:28:36 +0530 Subject: [PATCH] test: fix hanging tests and centralize agent mocks - Add global test setup in agents lib for @langchain/openai mock - Fix ContactsService mock in persistent-context middleware spec - Resolve open handles in web api chat route tests by consuming streams - Configure sales lib jest to pass with no tests COAUTHOR="Co-authored-by: Antigravity AI --- .github/workflows/ci.yml | 4 + apps/web/package.json | 5 + apps/web/src/app/api/chat/route.test.ts | 175 +++++++++--------- libs/agents/jest.config.cts | 17 +- .../persistent-context.middleware.spec.ts | 4 +- libs/agents/src/test-setup.ts | 15 ++ libs/agents/tsconfig.lib.json | 3 +- libs/agents/tsconfig.spec.json | 3 +- libs/sales/jest.config.cts | 17 +- 9 files changed, 137 insertions(+), 106 deletions(-) create mode 100644 libs/agents/src/test-setup.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9712a82..e110273 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,6 +90,10 @@ jobs: - name: Build run: npx nx affected -t build --parallel=50% --nxBail + # Tests (Sequential) + - name: Unit tests + run: npx nx affected -t test --parallel=1 --nxBail + - name: E2E tests run: npx nx affected -t e2e --exclude=backend-e2e --nxBail diff --git a/apps/web/package.json b/apps/web/package.json index 45c7d41..45c72ee 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -36,6 +36,11 @@ "options": { "command": "export FLY_APP=$(grep '^app = ' apps/web/fly.toml | cut -d \"'\" -f 2) && export GIT_SHA=$(git rev-parse --short HEAD) && flyctl auth docker && docker push registry.fly.io/$FLY_APP:$GIT_SHA && fly deploy --image registry.fly.io/$FLY_APP:$GIT_SHA -c apps/web/fly.toml" } + }, + "test": { + "options": { + "command": "jest --forceExit" + } } } } diff --git a/apps/web/src/app/api/chat/route.test.ts b/apps/web/src/app/api/chat/route.test.ts index 966d431..ad32c46 100644 --- a/apps/web/src/app/api/chat/route.test.ts +++ b/apps/web/src/app/api/chat/route.test.ts @@ -1,13 +1,13 @@ /** * @jest-environment node */ -import { POST } from './route'; -import { NextRequest } from 'next/server'; +import { POST } from "./route"; +import { NextRequest } from "next/server"; // Mock Next.js cookies -jest.mock('next/headers', () => ({ +jest.mock("next/headers", () => ({ cookies: jest.fn(() => ({ - toString: () => 'mock-session-cookie=value', + toString: () => "mock-session-cookie=value", })), })); @@ -15,46 +15,46 @@ jest.mock('next/headers', () => ({ const mockFetch = jest.fn(); global.fetch = mockFetch as any; -describe('/api/chat POST', () => { +describe("/api/chat POST", () => { beforeEach(() => { jest.clearAllMocks(); // Suppress console.log in tests - jest.spyOn(console, 'log').mockImplementation(); - jest.spyOn(console, 'error').mockImplementation(); + jest.spyOn(console, "log").mockImplementation(); + jest.spyOn(console, "error").mockImplementation(); }); afterEach(() => { jest.restoreAllMocks(); }); - it('extracts chatId from referer header and forwards to backend', async () => { + it("extracts chatId from referer header and forwards to backend", async () => { const mockRequestBody = { - id: 'test-id', + id: "test-id", messages: [ { - parts: [{ type: 'text', text: 'Hello' }], - id: 'msg-1', - role: 'user', + parts: [{ type: "text", text: "Hello" }], + id: "msg-1", + role: "user", }, ], - trigger: 'submit-message', + trigger: "submit-message", }; - const mockBackendResponse = new Response('mock streaming response', { + const mockBackendResponse = new Response("mock streaming response", { status: 200, headers: { - 'Content-Type': 'text/event-stream', + "Content-Type": "text/event-stream", }, }); (global.fetch as jest.Mock).mockResolvedValue(mockBackendResponse); // Create mock NextRequest with referer header - const request = new NextRequest('http://localhost:3000/api/chat', { - method: 'POST', + const request = new NextRequest("http://localhost:3000/api/chat", { + method: "POST", headers: { - 'content-type': 'application/json', - referer: 'http://localhost:3000/chat/test-chat-123', + "content-type": "application/json", + referer: "http://localhost:3000/chat/test-chat-123", }, body: JSON.stringify(mockRequestBody), }); @@ -63,47 +63,49 @@ describe('/api/chat POST', () => { // Verify fetch was called with correct URL and body including chatId expect(global.fetch).toHaveBeenCalledWith( - 'http://localhost:3001/api/v1/chat', + "http://localhost:3001/api/v1/chat", expect.objectContaining({ - method: 'POST', + method: "POST", headers: expect.objectContaining({ - 'Content-Type': 'application/json', - Cookie: 'mock-session-cookie=value', - Accept: 'text/event-stream', + "Content-Type": "application/json", + Cookie: "mock-session-cookie=value", + Accept: "text/event-stream", }), body: JSON.stringify({ ...mockRequestBody, - chatId: 'test-chat-123', + chatId: "test-chat-123", }), }), ); expect(response.status).toBe(200); + await response.text(); // Consume stream to prevent Jest open handles }); - it('handles missing referer header gracefully', async () => { + it("handles missing referer header gracefully", async () => { const mockRequestBody = { - id: 'test-id', + id: "test-id", messages: [], - trigger: 'submit-message', + trigger: "submit-message", }; - const mockBackendResponse = new Response('mock response', { + const mockBackendResponse = new Response("mock response", { status: 200, }); (global.fetch as jest.Mock).mockResolvedValue(mockBackendResponse); // Create request without referer header - const request = new NextRequest('http://localhost:3000/api/chat', { - method: 'POST', + const request = new NextRequest("http://localhost:3000/api/chat", { + method: "POST", headers: { - 'content-type': 'application/json', + "content-type": "application/json", }, body: JSON.stringify(mockRequestBody), }); - await POST(request); + const response = await POST(request); + await response.text(); // Verify chatId is null when referer is missing const fetchCall = (global.fetch as jest.Mock).mock.calls[0]; @@ -113,49 +115,50 @@ describe('/api/chat POST', () => { expect(parsedBody.chatId).toBeNull(); }); - it('extracts chatId correctly from various URL formats', async () => { + it("extracts chatId correctly from various URL formats", async () => { const testCases = [ { - referer: 'http://localhost:3000/chat/abc123', - expectedChatId: 'abc123', + referer: "http://localhost:3000/chat/abc123", + expectedChatId: "abc123", }, { - referer: 'http://localhost:3000/chat/test-chat-with-dashes', - expectedChatId: 'test-chat-with-dashes', + referer: "http://localhost:3000/chat/test-chat-with-dashes", + expectedChatId: "test-chat-with-dashes", }, { - referer: 'http://localhost:3000/chat/chat_123_test', - expectedChatId: 'chat_123_test', + referer: "http://localhost:3000/chat/chat_123_test", + expectedChatId: "chat_123_test", }, { - referer: 'http://localhost:3000/chat/abc123?query=param', - expectedChatId: 'abc123', + referer: "http://localhost:3000/chat/abc123?query=param", + expectedChatId: "abc123", }, { - referer: 'http://localhost:3000/chat/abc123#hash', - expectedChatId: 'abc123', + referer: "http://localhost:3000/chat/abc123#hash", + expectedChatId: "abc123", }, ]; for (const { referer, expectedChatId } of testCases) { jest.clearAllMocks(); - const mockBackendResponse = new Response('mock response', { + const mockBackendResponse = new Response("mock response", { status: 200, }); (global.fetch as jest.Mock).mockResolvedValue(mockBackendResponse); - const request = new NextRequest('http://localhost:3000/api/chat', { - method: 'POST', + const request = new NextRequest("http://localhost:3000/api/chat", { + method: "POST", headers: { - 'content-type': 'application/json', + "content-type": "application/json", referer, }, body: JSON.stringify({ messages: [] }), }); - await POST(request); + const response = await POST(request); + await response.text(); const fetchCall = (global.fetch as jest.Mock).mock.calls[0]; const bodyArg = fetchCall[1].body; @@ -165,23 +168,23 @@ describe('/api/chat POST', () => { } }); - it('forwards backend errors to the client', async () => { + it("forwards backend errors to the client", async () => { const mockRequestBody = { - id: 'test-id', + id: "test-id", messages: [], }; - const mockBackendResponse = new Response('Backend error message', { + const mockBackendResponse = new Response("Backend error message", { status: 403, }); (global.fetch as jest.Mock).mockResolvedValue(mockBackendResponse); - const request = new NextRequest('http://localhost:3000/api/chat', { - method: 'POST', + const request = new NextRequest("http://localhost:3000/api/chat", { + method: "POST", headers: { - 'content-type': 'application/json', - referer: 'http://localhost:3000/chat/test-123', + "content-type": "application/json", + referer: "http://localhost:3000/chat/test-123", }, body: JSON.stringify(mockRequestBody), }); @@ -190,22 +193,22 @@ describe('/api/chat POST', () => { expect(response.status).toBe(403); const text = await response.text(); - expect(text).toBe('Backend error message'); + expect(text).toBe("Backend error message"); }); - it('handles backend fetch errors', async () => { + it("handles backend fetch errors", async () => { const mockRequestBody = { - id: 'test-id', + id: "test-id", messages: [], }; - (global.fetch as jest.Mock).mockRejectedValue(new Error('Network error')); + (global.fetch as jest.Mock).mockRejectedValue(new Error("Network error")); - const request = new NextRequest('http://localhost:3000/api/chat', { - method: 'POST', + const request = new NextRequest("http://localhost:3000/api/chat", { + method: "POST", headers: { - 'content-type': 'application/json', - referer: 'http://localhost:3000/chat/test-123', + "content-type": "application/json", + referer: "http://localhost:3000/chat/test-123", }, body: JSON.stringify(mockRequestBody), }); @@ -214,53 +217,54 @@ describe('/api/chat POST', () => { expect(response.status).toBe(500); const json = await response.json(); - expect(json.error).toBe('Network error'); + expect(json.error).toBe("Network error"); }); - it('includes correct headers when forwarding to backend', async () => { - const mockBackendResponse = new Response('ok', { status: 200 }); + it("includes correct headers when forwarding to backend", async () => { + const mockBackendResponse = new Response("ok", { status: 200 }); (global.fetch as jest.Mock).mockResolvedValue(mockBackendResponse); - const request = new NextRequest('http://localhost:3000/api/chat', { - method: 'POST', + const request = new NextRequest("http://localhost:3000/api/chat", { + method: "POST", headers: { - 'content-type': 'application/json', - referer: 'http://localhost:3000/chat/test-123', + "content-type": "application/json", + referer: "http://localhost:3000/chat/test-123", }, body: JSON.stringify({ messages: [] }), }); - await POST(request); + const response = await POST(request); + await response.text(); expect(global.fetch).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ headers: expect.objectContaining({ - 'Content-Type': 'application/json', - Cookie: 'mock-session-cookie=value', - Accept: 'text/event-stream', + "Content-Type": "application/json", + Cookie: "mock-session-cookie=value", + Accept: "text/event-stream", }), - credentials: 'include', + credentials: "include", }), ); }); - it('preserves streaming response from backend', async () => { + it("preserves streaming response from backend", async () => { const mockStreamBody = 'data: {"content":"test"}\n\n'; const mockBackendResponse = new Response(mockStreamBody, { status: 200, headers: { - 'Content-Type': 'text/event-stream', + "Content-Type": "text/event-stream", }, }); (global.fetch as jest.Mock).mockResolvedValue(mockBackendResponse); - const request = new NextRequest('http://localhost:3000/api/chat', { - method: 'POST', + const request = new NextRequest("http://localhost:3000/api/chat", { + method: "POST", headers: { - 'content-type': 'application/json', - referer: 'http://localhost:3000/chat/test-123', + "content-type": "application/json", + referer: "http://localhost:3000/chat/test-123", }, body: JSON.stringify({ messages: [] }), }); @@ -268,8 +272,9 @@ describe('/api/chat POST', () => { const response = await POST(request); // Verify response has SSE headers - expect(response.headers.get('Content-Type')).toBe('text/event-stream'); - expect(response.headers.get('Cache-Control')).toBe('no-cache'); - expect(response.headers.get('Connection')).toBe('keep-alive'); + expect(response.headers.get("Content-Type")).toBe("text/event-stream"); + expect(response.headers.get("Cache-Control")).toBe("no-cache"); + expect(response.headers.get("Connection")).toBe("keep-alive"); + await response.text(); }); }); diff --git a/libs/agents/jest.config.cts b/libs/agents/jest.config.cts index 01a9b3f..543bb11 100644 --- a/libs/agents/jest.config.cts +++ b/libs/agents/jest.config.cts @@ -1,21 +1,22 @@ /* eslint-disable */ -const { readFileSync } = require('fs'); +const { readFileSync } = require("fs"); // Reading the SWC compilation config for the spec files const swcJestConfig = JSON.parse( - readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8'), + readFileSync(`${__dirname}/.spec.swcrc`, "utf-8"), ); // Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves swcJestConfig.swcrc = false; module.exports = { - displayName: '@zuko/agents', - preset: '../../jest.preset.js', - testEnvironment: 'node', + displayName: "@zuko/agents", + preset: "../../jest.preset.js", + testEnvironment: "node", transform: { - '^.+\\.[tj]s$': ['@swc/jest', swcJestConfig], + "^.+\\.[tj]s$": ["@swc/jest", swcJestConfig], }, - moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: 'test-output/jest/coverage', + moduleFileExtensions: ["ts", "js", "html"], + coverageDirectory: "test-output/jest/coverage", + setupFilesAfterEnv: ["/src/test-setup.ts"], }; diff --git a/libs/agents/src/lib/middleware/persistent-context.middleware.spec.ts b/libs/agents/src/lib/middleware/persistent-context.middleware.spec.ts index 92a48a8..7f50594 100644 --- a/libs/agents/src/lib/middleware/persistent-context.middleware.spec.ts +++ b/libs/agents/src/lib/middleware/persistent-context.middleware.spec.ts @@ -16,9 +16,7 @@ class PrismaService { // Mock services needed for tests const mockContactsService = { - findOne: jest - .fn() - .mockResolvedValue({ id: 1, firstName: "John", lastName: "Doe" }), + findById: jest.fn().mockResolvedValue({ id: 1, name: "John Doe" }), findOwner: jest.fn().mockResolvedValue({ id: 1, name: "Owner" }), } as any; diff --git a/libs/agents/src/test-setup.ts b/libs/agents/src/test-setup.ts new file mode 100644 index 0000000..05f1f9c --- /dev/null +++ b/libs/agents/src/test-setup.ts @@ -0,0 +1,15 @@ +jest.mock("@langchain/openai", () => { + const { FakeListChatModel } = require("@langchain/core/utils/testing"); + class MockChatOpenAI extends FakeListChatModel { + constructor() { + super({ responses: ["Mock response"] }); + } + bindTools() { + return this; + } + withStructuredOutput() { + return this; + } + } + return { ChatOpenAI: MockChatOpenAI }; +}); diff --git a/libs/agents/tsconfig.lib.json b/libs/agents/tsconfig.lib.json index ec44d5b..0581300 100644 --- a/libs/agents/tsconfig.lib.json +++ b/libs/agents/tsconfig.lib.json @@ -21,6 +21,7 @@ "jest.config.ts", "jest.config.cts", "src/**/*.spec.ts", - "src/**/*.test.ts" + "src/**/*.test.ts", + "src/test-setup.ts" ] } diff --git a/libs/agents/tsconfig.spec.json b/libs/agents/tsconfig.spec.json index 6e2300b..c3fc948 100644 --- a/libs/agents/tsconfig.spec.json +++ b/libs/agents/tsconfig.spec.json @@ -9,7 +9,8 @@ "jest.config.cts", "src/**/*.test.ts", "src/**/*.spec.ts", - "src/**/*.d.ts" + "src/**/*.d.ts", + "src/test-setup.ts" ], "references": [ { diff --git a/libs/sales/jest.config.cts b/libs/sales/jest.config.cts index 10f15b9..0a7852b 100644 --- a/libs/sales/jest.config.cts +++ b/libs/sales/jest.config.cts @@ -1,21 +1,22 @@ /* eslint-disable */ -const { readFileSync } = require('fs'); +const { readFileSync } = require("fs"); // Reading the SWC compilation config for the spec files const swcJestConfig = JSON.parse( - readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8'), + readFileSync(`${__dirname}/.spec.swcrc`, "utf-8"), ); // Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves swcJestConfig.swcrc = false; module.exports = { - displayName: '@zuko/sales', - preset: '../../jest.preset.js', - testEnvironment: 'node', + displayName: "@zuko/sales", + preset: "../../jest.preset.js", + testEnvironment: "node", transform: { - '^.+\\.[tj]s$': ['@swc/jest', swcJestConfig], + "^.+\\.[tj]s$": ["@swc/jest", swcJestConfig], }, - moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: 'test-output/jest/coverage', + moduleFileExtensions: ["ts", "js", "html"], + coverageDirectory: "test-output/jest/coverage", + passWithNoTests: true, };