- archive.entities.list / archive.entities.restore für alle per MCP archivierbaren Entitäten mit Berechtigungsprüfung je Entität und Historieneintrag - accounting.bank_accounts.balances liefert Stände von Bankkonten und Kassenbüchern, optional zu einem Stichtag Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
67 lines
3.8 KiB
TypeScript
67 lines
3.8 KiB
TypeScript
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 }
|
|
})
|
|
|
|
// Document/status tools load related records; their dedicated tests cover filtered pagination.
|
|
for (const tool of mcpTools.filter((tool) => (tool.inputSchema.properties as any)?.offset && !tool.name.startsWith("documents.")
|
|
&& tool.name !== "accounting.outgoing_documents.status.list")) {
|
|
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", isAdmin: true } as any
|
|
const properties = tool.inputSchema.properties as any
|
|
const ids: unknown[] = []
|
|
let offset = 0
|
|
for (let page = 0; page < 3; page++) {
|
|
const result: any = await tool.handler(context, { ...(properties.id ? { id: 1 } : {}), ...(properties.entity ? { entity: properties.entity.enum[0] } : {}), 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 })
|
|
})
|