Missing Commits from previous merge (#321)

* fix: Maintain this binding for pagination

* chore: fix tests to match core sdk

* fix: Upgrade Dockerfile and uv
This commit is contained in:
Vineeth Voruganti 2026-01-13 12:14:57 -05:00 committed by GitHub
parent ee12164835
commit 5978c670de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 251 additions and 239 deletions

View File

@ -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

View File

@ -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"]

View File

@ -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",

View File

@ -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);

View File

@ -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: [],

View File

@ -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);

View File

@ -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'
)
})
})
})

View File

@ -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);

View File

@ -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