feat(mcp): paginate record lists and describe document inputs
This commit is contained in:
63
backend/tests/mcpPagination.test.ts
Normal file
63
backend/tests/mcpPagination.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import assert from "node:assert/strict"
|
||||
import test from "node:test"
|
||||
import { getTableName } from "drizzle-orm"
|
||||
import { drizzle } from "drizzle-orm/pg-proxy"
|
||||
import * as schema from "../db/schema"
|
||||
import { mcpTools } from "../src/mcp/registry"
|
||||
import { limitFromArgs, offsetFromArgs, paginatedResult } from "../src/mcp/pagination"
|
||||
|
||||
const tables = Object.values(schema).filter((value: any) => {
|
||||
try { return typeof getTableName(value) === "string" } catch { return false }
|
||||
})
|
||||
|
||||
for (const tool of mcpTools.filter((tool) => (tool.inputSchema.properties as any)?.offset)) {
|
||||
test(`${tool.name}: retrieves 205 records across stable, bounded pages`, async () => {
|
||||
const db = drizzle(async (sql, params) => {
|
||||
if (sql.includes('from "tenants"')) return { rows: [["skr03"]] }
|
||||
const tableName = sql.match(/from "(\w+)"/)?.[1]
|
||||
const table = tables.find((table: any) => getTableName(table) === tableName) as any
|
||||
assert.ok(table, sql)
|
||||
assert.match(sql, /order by .*"id"/)
|
||||
if (table.tenant || table.tenantId) {
|
||||
assert.match(sql, /where .*"tenant(?:_id)?" = \$\d+/)
|
||||
assert.ok(params.includes(7), "active tenant must be bound")
|
||||
}
|
||||
if (table.archived) assert.match(sql, /"archived" = \$\d+/)
|
||||
const limitParam = sql.match(/limit \$(\d+)/)
|
||||
const offsetParam = sql.match(/offset \$(\d+)/)
|
||||
assert.ok(limitParam, "query must be bounded")
|
||||
const limit = Number(params[Number(limitParam[1]) - 1])
|
||||
const offset = offsetParam ? Number(params[Number(offsetParam[1]) - 1]) : 0
|
||||
assert.equal(limit, 101, "fetch one extra row for hasMore")
|
||||
const selected = sql.slice(7, sql.indexOf(" from ")).split(", ")
|
||||
const idIndex = selected.findIndex((column) => /"id"$/.test(column))
|
||||
assert.ok(idIndex >= 0)
|
||||
return { rows: Array.from({ length: Math.min(limit, Math.max(0, 205 - offset)) }, (_, i) => {
|
||||
const values = selected.map(() => null as unknown)
|
||||
values[idIndex] = offset + i + 1
|
||||
return values
|
||||
}) }
|
||||
}, { schema })
|
||||
const context = { server: { db }, tenantId: 7, userId: "test" } as any
|
||||
const ids: unknown[] = []
|
||||
let offset = 0
|
||||
for (let page = 0; page < 3; page++) {
|
||||
const result: any = await tool.handler(context, { id: 1, limit: 100, offset })
|
||||
assert.equal(result.rows.length, page < 2 ? 100 : 5)
|
||||
assert.equal(result.pagination.hasMore, page < 2)
|
||||
assert.equal(result.pagination.nextOffset, page < 2 ? offset + 100 : null)
|
||||
ids.push(...result.rows.map((row: any) => Number(row.id)))
|
||||
offset = result.pagination.nextOffset
|
||||
}
|
||||
assert.deepEqual(ids, Array.from({ length: 205 }, (_, i) => i + 1))
|
||||
})
|
||||
}
|
||||
|
||||
test("pagination validates input and handles empty and exactly full final pages", () => {
|
||||
for (const limit of [-1, 0, 101, 1.5, "10", Infinity]) assert.throws(() => limitFromArgs({ limit }))
|
||||
for (const offset of [-1, 1.5, "10", Infinity]) assert.throws(() => offsetFromArgs({ offset }))
|
||||
assert.equal(limitFromArgs({}), 25)
|
||||
assert.equal(limitFromArgs({}, 50), 50)
|
||||
assert.equal(paginatedResult([1, 2], { limit: 2 }).pagination.nextOffset, null)
|
||||
assert.deepEqual(paginatedResult([], { offset: 300 }).pagination, { limit: 25, offset: 300, hasMore: false, nextOffset: null })
|
||||
})
|
||||
Reference in New Issue
Block a user