diff --git a/.github/workflows/unified-tests.yml b/.github/workflows/unified-tests.yml index 40a10ba7..bfc27018 100644 --- a/.github/workflows/unified-tests.yml +++ b/.github/workflows/unified-tests.yml @@ -3,6 +3,9 @@ name: Unified Tests (Fly Runner) on: push: branches: [main] + paths: + - 'src/**' + - 'tests/**' permissions: contents: read @@ -58,7 +61,7 @@ jobs: which python3.12 - name: Install the project - run: uv sync --all-extras --dev + run: uv sync --all-extras - name: Run unified tests run: uv run python -m tests.unified.run diff --git a/Dockerfile b/Dockerfile index 02340993..30a8d09b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,38 +1,41 @@ # https://pythonspeed.com/articles/base-image-python-docker-images/ # https://testdriven.io/blog/docker-best-practices/ -FROM python:3.11-slim-bullseye +FROM python:3.13-slim-bookworm -COPY --from=ghcr.io/astral-sh/uv:0.4.9 /uv /bin/uv +COPY --from=ghcr.io/astral-sh/uv:0.9.24 /uv /bin/uv # Set Working directory WORKDIR /app -RUN addgroup --system app && adduser --system --group app -RUN chown -R app:app /app -USER app - # Enable bytecode compilation ENV UV_COMPILE_BYTECODE=1 # Copy from the cache instead of linking since it's a mounted volume ENV UV_LINK_MODE=copy +# Python optimizations +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + # Install the project's dependencies using the lockfile and settings RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ - uv sync --frozen --no-install-project --no-dev + uv sync --frozen --no-install-project --no-group dev # Copy only requirements to cache them in docker layer COPY uv.lock pyproject.toml /app/ # Sync the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --frozen --no-dev + uv sync --frozen --no-group dev # Place executables in the environment at the front of the path ENV PATH="/app/.venv/bin:$PATH" +# Create non-root user and set ownership +RUN addgroup --system app && adduser --system --group app && chown -R app:app /app + COPY --chown=app:app src/ /app/src/ COPY --chown=app:app migrations/ /app/migrations/ COPY --chown=app:app scripts/ /app/scripts/ @@ -40,7 +43,12 @@ COPY --chown=app:app alembic.ini /app/alembic.ini # Copy config files - this will copy config.toml if it exists, and config.toml.example COPY --chown=app:app config.toml* /app/ +# Switch to non-root user +USER app + EXPOSE 8000 -# https://stackoverflow.com/questions/29663459/python-app-does-not-print-anything-when-running-detached-in-docker +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/openapi.json')" || exit 1 + CMD ["fastapi", "run", "--host", "0.0.0.0", "src/main.py"] diff --git a/pyproject.toml b/pyproject.toml index d18f697e..df367820 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,8 +37,8 @@ dependencies = [ "cashews[redis]==7.4.4", "scikit-learn>=1.6.0", ] -[tool.uv] -dev-dependencies = [ +[dependency-groups] +dev = [ "pytest>=8.2.2", "sqlalchemy-utils>=0.41.2", "pytest-asyncio>=0.23.7", diff --git a/sdks/typescript/__tests__/client.test.ts b/sdks/typescript/__tests__/client.test.ts index 5c3c18fa..2708972b 100644 --- a/sdks/typescript/__tests__/client.test.ts +++ b/sdks/typescript/__tests__/client.test.ts @@ -140,7 +140,7 @@ describe('Honcho Client', () => { ], total: 2, size: 2, - hasNextPage: false, + hasNextPage: () => false, }; mockClient.workspaces.peers.list.mockResolvedValue(mockPeersData); @@ -155,7 +155,7 @@ describe('Honcho Client', () => { items: [], total: 0, size: 0, - hasNextPage: false, + hasNextPage: () => false, }; mockClient.workspaces.peers.list.mockResolvedValue(mockPeersData); @@ -218,7 +218,7 @@ describe('Honcho Client', () => { ], total: 2, size: 2, - hasNextPage: false, + hasNextPage: () => false, }; mockClient.workspaces.sessions.list.mockResolvedValue(mockSessionsData); @@ -233,7 +233,7 @@ describe('Honcho Client', () => { items: [], total: 0, size: 0, - hasNextPage: false, + hasNextPage: () => false, }; mockClient.workspaces.sessions.list.mockResolvedValue(mockSessionsData); diff --git a/sdks/typescript/__tests__/integration.test.ts b/sdks/typescript/__tests__/integration.test.ts index d08914f7..f5d3c6e9 100644 --- a/sdks/typescript/__tests__/integration.test.ts +++ b/sdks/typescript/__tests__/integration.test.ts @@ -182,7 +182,7 @@ describe('Honcho SDK Integration Tests', () => { ], total: 2, size: 2, - hasNextPage: false, + hasNextPage: () => false, [Symbol.asyncIterator]: async function*() { for (const item of this.items) { yield item @@ -422,7 +422,7 @@ describe('Honcho SDK Integration Tests', () => { mockWorkspacesApi.workspaces.peers.list.mockResolvedValue({ items: [], total: 0, - hasNextPage: false, + hasNextPage: () => false, }) mockWorkspacesApi.workspaces.sessions.context.mockResolvedValue({ messages: [], diff --git a/sdks/typescript/__tests__/metadata_caching.test.ts b/sdks/typescript/__tests__/metadata_caching.test.ts index 1767d2ff..fd44eb6f 100644 --- a/sdks/typescript/__tests__/metadata_caching.test.ts +++ b/sdks/typescript/__tests__/metadata_caching.test.ts @@ -273,7 +273,7 @@ describe('Metadata and Configuration Caching', () => { ], total: 2, size: 2, - hasNextPage: false, + hasNextPage: () => false, }; mockClient.workspaces.peers.list.mockResolvedValue(mockPeersData); @@ -297,7 +297,7 @@ describe('Metadata and Configuration Caching', () => { ], total: 1, size: 1, - hasNextPage: false, + hasNextPage: () => false, }; mockClient.workspaces.peers.list.mockResolvedValue(mockPeersData); @@ -444,7 +444,7 @@ describe('Metadata and Configuration Caching', () => { ], total: 2, size: 2, - hasNextPage: false, + hasNextPage: () => false, }; mockClient.workspaces.sessions.list.mockResolvedValue(mockSessionsData); @@ -468,7 +468,7 @@ describe('Metadata and Configuration Caching', () => { ], total: 1, size: 1, - hasNextPage: false, + hasNextPage: () => false, }; mockClient.workspaces.sessions.list.mockResolvedValue(mockSessionsData); diff --git a/sdks/typescript/__tests__/pagination.test.ts b/sdks/typescript/__tests__/pagination.test.ts index 2252b3d6..be69d6ea 100644 --- a/sdks/typescript/__tests__/pagination.test.ts +++ b/sdks/typescript/__tests__/pagination.test.ts @@ -1,15 +1,15 @@ -import { Page } from "../src/pagination"; +import { Page } from '../src/pagination' -describe("Page", () => { - let mockOriginalPage: any; - let mockItems: any[]; +describe('Page', () => { + let mockOriginalPage: any + let mockItems: any[] beforeEach(() => { mockItems = [ - { id: "item1", name: "Item 1" }, - { id: "item2", name: "Item 2" }, - { id: "item3", name: "Item 3" }, - ]; + { id: 'item1', name: 'Item 1' }, + { id: 'item2', name: 'Item 2' }, + { id: 'item3', name: 'Item 3' }, + ] mockOriginalPage = { items: mockItems, @@ -20,311 +20,312 @@ describe("Page", () => { hasNextPage: () => true, [Symbol.asyncIterator]: async function*() { for (const item of mockItems) { - yield item; + yield item } }, - } as any; - }); + } as any + }) - describe("constructor", () => { - it("should initialize with original page", () => { - const page = new Page(mockOriginalPage); + describe('constructor', () => { + it('should initialize with original page', () => { + const page = new Page(mockOriginalPage) - expect(page["_originalPage"]).toBe(mockOriginalPage); - expect(page["_transformFunc"]).toBeUndefined(); - }); + expect(page['_originalPage']).toBe(mockOriginalPage) + expect(page['_transformFunc']).toBeUndefined() + }) - it("should initialize with transform function", () => { - const transformFunc = (item: any) => ({ ...item, transformed: true }); - const page = new Page(mockOriginalPage, transformFunc); + it('should initialize with transform function', () => { + const transformFunc = (item: any) => ({ ...item, transformed: true }) + const page = new Page(mockOriginalPage, transformFunc) - expect(page["_originalPage"]).toBe(mockOriginalPage); - expect(page["_transformFunc"]).toBe(transformFunc); - }); - }); + expect(page['_originalPage']).toBe(mockOriginalPage) + expect(page['_transformFunc']).toBe(transformFunc) + }) + }) - describe("Symbol.asyncIterator", () => { - it("should iterate through items without transform", async () => { - const page = new Page(mockOriginalPage); - const items: any[] = []; + describe('Symbol.asyncIterator', () => { + it('should iterate through items without transform', async () => { + const page = new Page(mockOriginalPage) + const items: any[] = [] for await (const item of page) { - items.push(item); + items.push(item) } - expect(items).toEqual(mockItems); - }); + expect(items).toEqual(mockItems) + }) - it("should iterate through items with transform", async () => { - const transformFunc = (item: any) => ({ ...item, transformed: true }); - const page = new Page(mockOriginalPage, transformFunc); - const items: any[] = []; + it('should iterate through items with transform', async () => { + const transformFunc = (item: any) => ({ ...item, transformed: true }) + const page = new Page(mockOriginalPage, transformFunc) + const items: any[] = [] for await (const item of page) { - items.push(item); + items.push(item) } expect(items).toEqual([ - { id: "item1", name: "Item 1", transformed: true }, - { id: "item2", name: "Item 2", transformed: true }, - { id: "item3", name: "Item 3", transformed: true }, - ]); - }); + { id: 'item1', name: 'Item 1', transformed: true }, + { id: 'item2', name: 'Item 2', transformed: true }, + { id: 'item3', name: 'Item 3', transformed: true }, + ]) + }) - it("should handle transform function that throws error", async () => { + it('should handle transform function that throws error', async () => { const errorTransform = () => { - throw new Error("Transform error"); - }; - const page = new Page(mockOriginalPage, errorTransform); + throw new Error('Transform error') + } + const page = new Page(mockOriginalPage, errorTransform) const iterate = async () => { for await (const item of page) { // This should throw } - }; + } - await expect(iterate()).rejects.toThrow("Transform error"); - }); - }); + await expect(iterate()).rejects.toThrow('Transform error') + }) + }) - describe("get", () => { - it("should get item by index without transform", () => { - const page = new Page(mockOriginalPage); + describe('get', () => { + it('should get item by index without transform', () => { + const page = new Page(mockOriginalPage) - const item = page.get(1); + const item = page.get(1) - expect(item).toEqual(mockItems[1]); - }); + expect(item).toEqual(mockItems[1]) + }) - it("should get item by index with transform", () => { - const transformFunc = (item: any) => ({ ...item, transformed: true }); - const page = new Page(mockOriginalPage, transformFunc); + it('should get item by index with transform', () => { + const transformFunc = (item: any) => ({ ...item, transformed: true }) + const page = new Page(mockOriginalPage, transformFunc) - const item = page.get(1); + const item = page.get(1) - expect(item).toEqual({ ...mockItems[1], transformed: true }); - }); + expect(item).toEqual({ ...mockItems[1], transformed: true }) + }) - it("should handle out of bounds index", () => { - const page = new Page(mockOriginalPage); + it('should handle out of bounds index', () => { + const page = new Page(mockOriginalPage) expect(() => page.get(999)).toThrow( - "Index 999 is out of bounds for page with 3 items", - ); - }); - }); + 'Index 999 is out of bounds for page with 3 items' + ) + }) + }) - describe("length getter", () => { - it("should return length of items array", () => { - const page = new Page(mockOriginalPage); + describe('length getter', () => { + it('should return length of items array', () => { + const page = new Page(mockOriginalPage) - expect(page.length).toBe(3); - }); + expect(page.length).toBe(3) + }) - it("should handle empty items array", () => { - const emptyPage = { ...mockOriginalPage, items: [] }; - const page = new Page(emptyPage); + it('should handle empty items array', () => { + const emptyPage = { ...mockOriginalPage, items: [] } + const page = new Page(emptyPage) - expect(page.length).toBe(0); - }); + expect(page.length).toBe(0) + }) - it("should handle undefined items", () => { - const noItemsPage = { ...mockOriginalPage, items: undefined }; - const page = new Page(noItemsPage); + it('should handle undefined items', () => { + const noItemsPage = { ...mockOriginalPage, items: undefined } + const page = new Page(noItemsPage) - expect(page.length).toBe(0); - }); - }); + expect(page.length).toBe(0) + }) + }) - describe("items getter", () => { - it("should return items array without transform", () => { - const page = new Page(mockOriginalPage); + describe('items getter', () => { + it('should return items array without transform', () => { + const page = new Page(mockOriginalPage) - const data = page.items; + const data = page.items - expect(data).toEqual(mockItems); - }); + expect(data).toEqual(mockItems) + }) - it("should return items array with transform", () => { - const transformFunc = (item: any) => ({ ...item, transformed: true }); - const page = new Page(mockOriginalPage, transformFunc); + it('should return items array with transform', () => { + const transformFunc = (item: any) => ({ ...item, transformed: true }) + const page = new Page(mockOriginalPage, transformFunc) - const data = page.items; + const data = page.items expect(data).toEqual([ - { id: "item1", name: "Item 1", transformed: true }, - { id: "item2", name: "Item 2", transformed: true }, - { id: "item3", name: "Item 3", transformed: true }, - ]); - }); + { id: 'item1', name: 'Item 1', transformed: true }, + { id: 'item2', name: 'Item 2', transformed: true }, + { id: 'item3', name: 'Item 3', transformed: true }, + ]) + }) - it("should handle transform function returning null", () => { - const nullTransform = () => null; - const page = new Page(mockOriginalPage, nullTransform); + it('should handle transform function returning null', () => { + const nullTransform = () => null + const page = new Page(mockOriginalPage, nullTransform) - const data = page.items; + const data = page.items - expect(data).toEqual([null, null, null]); - }); - }); + expect(data).toEqual([null, null, null]) + }) + }) - describe("pagination metadata getters", () => { - it("should return total from original page", () => { - const page = new Page(mockOriginalPage); + describe('pagination metadata getters', () => { + it('should return total from original page', () => { + const page = new Page(mockOriginalPage) - expect(page.total).toBe(10); - }); + expect(page.total).toBe(10) + }) - it("should return page number from original page", () => { - const page = new Page(mockOriginalPage); + it('should return page number from original page', () => { + const page = new Page(mockOriginalPage) - expect(page.page).toBe(1); - }); + expect(page.page).toBe(1) + }) - it("should return size from original page", () => { - const page = new Page(mockOriginalPage); + it('should return size from original page', () => { + const page = new Page(mockOriginalPage) - expect(page.size).toBe(3); - }); + expect(page.size).toBe(3) + }) - it("should return pages from original page", () => { - const page = new Page(mockOriginalPage); + it('should return pages from original page', () => { + const page = new Page(mockOriginalPage) - expect(page.pages).toBe(4); - }); + expect(page.pages).toBe(4) + }) - it("should handle undefined metadata", () => { - const minimalPage = { items: mockItems }; - const page = new Page(minimalPage as any); + it('should handle undefined metadata', () => { + const minimalPage = { items: mockItems } + const page = new Page(minimalPage as any) - expect(page.total).toBeUndefined(); - expect(page.page).toBeUndefined(); - expect(page.size).toBeUndefined(); - expect(page.pages).toBeUndefined(); - }); - }); + expect(page.total).toBeUndefined() + expect(page.page).toBeUndefined() + expect(page.size).toBeUndefined() + expect(page.pages).toBeUndefined() + }) + }) - describe("hasNextPage getter", () => { - it("should return true when hasNextPage is true", () => { - const page = new Page(mockOriginalPage); + describe('hasNextPage getter', () => { + it('should return true when hasNextPage is true', () => { + const page = new Page(mockOriginalPage) - expect(page.hasNextPage).toBe(true); - }); + expect(page.hasNextPage).toBe(true) + }) - it("should return false when hasNextPage is false", () => { - const lastPage = { ...mockOriginalPage, hasNextPage: false }; - const page = new Page(lastPage); + it('should return false when hasNextPage function returns false', () => { + const lastPage = { ...mockOriginalPage, hasNextPage: () => false } - expect(page.hasNextPage).toBe(false); - }); + const page = new Page(lastPage) - it("should call hasNextPage function if it is a function", () => { - const hasNextPageFn = jest.fn(() => true); - const pageWithFn = { ...mockOriginalPage, hasNextPage: hasNextPageFn }; - const page = new Page(pageWithFn); + expect(page.hasNextPage).toBe(false) + }) - const result = page.hasNextPage; + it('should call hasNextPage function if it is a function', () => { + const hasNextPageFn = jest.fn(() => true) + const pageWithFn = { ...mockOriginalPage, hasNextPage: hasNextPageFn } + const page = new Page(pageWithFn) - expect(result).toBe(true); - expect(hasNextPageFn).toHaveBeenCalled(); - }); + const result = page.hasNextPage - it("should return false when hasNextPage is undefined", () => { - const noNextPage = { ...mockOriginalPage }; - delete noNextPage.hasNextPage; - const page = new Page(noNextPage); + expect(result).toBe(true) + expect(hasNextPageFn).toHaveBeenCalled() + }) - expect(page.hasNextPage).toBe(false); - }); - }); + it('should return false when hasNextPage is undefined', () => { + const noNextPage = { ...mockOriginalPage } + delete noNextPage.hasNextPage + const page = new Page(noNextPage) - describe("getNextPage", () => { - it("should return next page with same transform function", async () => { + expect(page.hasNextPage).toBe(false) + }) + }) + + describe('getNextPage', () => { + it('should return next page with same transform function', async () => { const nextPageData = { - items: [{ id: "item4", name: "Item 4" }], + items: [{ id: 'item4', name: 'Item 4' }], size: 1, total: 10, page: 2, pages: 4, - hasNextPage: false, + hasNextPage: () => false, [Symbol.asyncIterator]: async function*() { for (const item of this.items) { - yield item; + yield item } }, - }; - const transformFunc = (item: any) => ({ ...item, transformed: true }); - mockOriginalPage.getNextPage = jest.fn().mockResolvedValue(nextPageData); - const page = new Page(mockOriginalPage, transformFunc); + } + const transformFunc = (item: any) => ({ ...item, transformed: true }) + mockOriginalPage.getNextPage = jest.fn().mockResolvedValue(nextPageData) + const page = new Page(mockOriginalPage, transformFunc) - const nextPage = await page.getNextPage(); + const nextPage = await page.getNextPage() - expect(nextPage).toBeInstanceOf(Page); - expect(nextPage!["_transformFunc"]).toBe(transformFunc); - expect(mockOriginalPage.getNextPage).toHaveBeenCalled(); - }); + expect(nextPage).toBeInstanceOf(Page) + expect(nextPage!['_transformFunc']).toBe(transformFunc) + expect(mockOriginalPage.getNextPage).toHaveBeenCalled() + }) - it("should return null when no next page", async () => { - mockOriginalPage.getNextPage = jest.fn().mockResolvedValue(null); - const page = new Page(mockOriginalPage); + it('should return null when no next page', async () => { + mockOriginalPage.getNextPage = jest.fn().mockResolvedValue(null) + const page = new Page(mockOriginalPage) - const nextPage = await page.getNextPage(); + const nextPage = await page.getNextPage() - expect(nextPage).toBeNull(); - }); + expect(nextPage).toBeNull() + }) - it("should return null when getNextPage returns undefined", async () => { - mockOriginalPage.getNextPage = jest.fn().mockResolvedValue(undefined); - const page = new Page(mockOriginalPage); + it('should return null when getNextPage returns undefined', async () => { + mockOriginalPage.getNextPage = jest.fn().mockResolvedValue(undefined) + const page = new Page(mockOriginalPage) - const nextPage = await page.getNextPage(); + const nextPage = await page.getNextPage() - expect(nextPage).toBeNull(); - }); + expect(nextPage).toBeNull() + }) - it("should return null when getNextPage method does not exist", async () => { - const pageWithoutNext = { ...mockOriginalPage }; - delete pageWithoutNext.getNextPage; - const page = new Page(pageWithoutNext); + it('should return null when getNextPage method does not exist', async () => { + const pageWithoutNext = { ...mockOriginalPage } + delete pageWithoutNext.getNextPage + const page = new Page(pageWithoutNext) - const nextPage = await page.getNextPage(); + const nextPage = await page.getNextPage() - expect(nextPage).toBeNull(); - }); + expect(nextPage).toBeNull() + }) - it("should propagate transform function to next page", async () => { + it('should propagate transform function to next page', async () => { const nextPageData = { - items: [{ id: "item4", name: "Item 4" }], + items: [{ id: 'item4', name: 'Item 4' }], [Symbol.asyncIterator]: async function*() { for (const item of this.items) { - yield item; + yield item } }, - }; - const transformFunc = (item: any) => ({ ...item, count: 999 }); - mockOriginalPage.getNextPage = jest.fn().mockResolvedValue(nextPageData); - const page = new Page(mockOriginalPage, transformFunc); + } + const transformFunc = (item: any) => ({ ...item, count: 999 }) + mockOriginalPage.getNextPage = jest.fn().mockResolvedValue(nextPageData) + const page = new Page(mockOriginalPage, transformFunc) - const nextPage = await page.getNextPage(); + const nextPage = await page.getNextPage() - expect(nextPage).not.toBeNull(); - const transformedItem = nextPage!.get(0); + expect(nextPage).not.toBeNull() + const transformedItem = nextPage!.get(0) expect(transformedItem).toEqual({ - id: "item4", - name: "Item 4", + id: 'item4', + name: 'Item 4', count: 999, - }); - }); + }) + }) - it("should handle error from getNextPage", async () => { + it('should handle error from getNextPage', async () => { mockOriginalPage.getNextPage = jest .fn() - .mockRejectedValue(new Error("Failed to get next page")); - const page = new Page(mockOriginalPage); + .mockRejectedValue(new Error('Failed to get next page')) + const page = new Page(mockOriginalPage) await expect(page.getNextPage()).rejects.toThrow( - "Failed to get next page", - ); - }); - }); -}); + 'Failed to get next page' + ) + }) + }) +}) diff --git a/sdks/typescript/__tests__/peer.test.ts b/sdks/typescript/__tests__/peer.test.ts index 8a3d26e2..ccc16f6e 100644 --- a/sdks/typescript/__tests__/peer.test.ts +++ b/sdks/typescript/__tests__/peer.test.ts @@ -165,7 +165,7 @@ describe('Peer', () => { ], total: 2, size: 2, - hasNextPage: false, + hasNextPage: () => false, }; mockClient.workspaces.peers.sessions.list.mockResolvedValue(mockSessionsData); @@ -184,7 +184,7 @@ describe('Peer', () => { items: [], total: 0, size: 0, - hasNextPage: false, + hasNextPage: () => false, }; mockClient.workspaces.peers.sessions.list.mockResolvedValue(mockSessionsData); diff --git a/sdks/typescript/__tests__/session.test.ts b/sdks/typescript/__tests__/session.test.ts index 409cfbee..5bfd7916 100644 --- a/sdks/typescript/__tests__/session.test.ts +++ b/sdks/typescript/__tests__/session.test.ts @@ -290,7 +290,7 @@ describe('Session', () => { ], total: 2, size: 2, - hasNextPage: false, + hasNextPage: () => false, } mockClient.workspaces.sessions.peers.list.mockResolvedValue(mockPeersData) @@ -311,7 +311,7 @@ describe('Session', () => { items: [], total: 0, size: 0, - hasNextPage: false, + hasNextPage: () => false, } mockClient.workspaces.sessions.peers.list.mockResolvedValue(mockPeersData) @@ -638,7 +638,7 @@ describe('Session', () => { ], total: 2, size: 2, - hasNextPage: false, + hasNextPage: () => false, } mockClient.workspaces.sessions.messages.list.mockResolvedValue( mockMessagesData @@ -659,7 +659,7 @@ describe('Session', () => { items: [], total: 0, size: 0, - hasNextPage: false, + hasNextPage: () => false, } mockClient.workspaces.sessions.messages.list.mockResolvedValue( mockMessagesData