mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 11:01:16 +01:00
90 lines
3.7 KiB
TypeScript
90 lines
3.7 KiB
TypeScript
import { Ajv2020 } from "ajv/dist/2020.js";
|
||
import { describe, expect, it, vi } from "vitest";
|
||
import type { Db } from "@paperclipai/db";
|
||
import { issueDocumentKeySchema } from "@paperclipai/shared";
|
||
import { documentService } from "../documents.js";
|
||
import { PaperclipRunnerToolAuthority } from "./paperclip-runner-tool-authority.js";
|
||
|
||
const binding = {
|
||
companyId: "00000000-0000-4000-8000-000000000001",
|
||
agentId: "00000000-0000-4000-8000-000000000002",
|
||
issueId: "00000000-0000-4000-8000-000000000003",
|
||
runId: "00000000-0000-4000-8000-000000000004",
|
||
casesEnabled: true,
|
||
};
|
||
const trimWhitespace = "\u0009\u000a\u000b\u000c\u000d\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff";
|
||
const keys = [
|
||
"reconciliation",
|
||
"reconciliation.md",
|
||
"a".repeat(64),
|
||
"a".repeat(65),
|
||
"Reconciliation",
|
||
`${trimWhitespace}${"a".repeat(64)}${trimWhitespace}`,
|
||
"K",
|
||
"K".repeat(64),
|
||
"K".repeat(65),
|
||
" a-b_1 ",
|
||
`\ufeff\u00a0\u2003${"a".repeat(64)}\r\n`,
|
||
" ".repeat(56) + "a".repeat(64),
|
||
"",
|
||
" \ufeff",
|
||
"../document",
|
||
"a/b",
|
||
"Research:v1.notes",
|
||
"\u0085key\u0085",
|
||
"_key",
|
||
"-key",
|
||
"a\u0000",
|
||
];
|
||
const operations = ["read_document", "write_document", "list_document_revisions"] as const;
|
||
const ajv = new Ajv2020({ allErrors: true, allowUnionTypes: true, strict: false });
|
||
const definitions = new PaperclipRunnerToolAuthority({} as Db, binding).definitions();
|
||
function input(operation: string, key: string) {
|
||
return operation === "write_document"
|
||
? { idempotencyKey: "synthetic", key, title: "Synthetic", body: "Synthetic", baseRevisionId: null }
|
||
: { key };
|
||
}
|
||
|
||
describe("ordinary task-document key contract parity", () => {
|
||
it.each(operations.flatMap(operation => keys.map(key => [operation, key] as const)))(
|
||
"%s matches real normalization for %j",
|
||
async (operation, key) => {
|
||
const expected = issueDocumentKeySchema.safeParse(key.trim().toLowerCase()).success;
|
||
const tool = definitions.find(definition => definition.name === operation)!;
|
||
expect(ajv.validate(tool.inputSchema as object, input(operation, key)), JSON.stringify(ajv.errors)).toBe(expected);
|
||
const select = vi.fn(() => {
|
||
throw new Error("synthetic_database_boundary");
|
||
});
|
||
const service = documentService({ select } as unknown as Db);
|
||
const call = operation === "read_document"
|
||
? service.getIssueDocumentByKey(binding.issueId, key)
|
||
: operation === "list_document_revisions"
|
||
? service.listIssueDocumentRevisions(binding.issueId, key)
|
||
: service.upsertIssueDocument({
|
||
issueId: binding.issueId,
|
||
key,
|
||
title: "Synthetic",
|
||
format: "markdown",
|
||
body: "Synthetic",
|
||
baseRevisionId: null,
|
||
});
|
||
await expect(call).rejects.toThrow(expected ? "synthetic_database_boundary" : "Invalid document key");
|
||
expect(select).toHaveBeenCalledTimes(expected ? 1 : 0);
|
||
},
|
||
);
|
||
|
||
it.each(operations)("%s retains the existing raw-input cap independently of normalization", operation => {
|
||
const key = " ".repeat(57) + "a".repeat(64);
|
||
expect(issueDocumentKeySchema.safeParse(key.trim().toLowerCase()).success).toBe(true);
|
||
const tool = definitions.find(definition => definition.name === operation)!;
|
||
expect(ajv.validate(tool.inputSchema as object, input(operation, key))).toBe(false);
|
||
});
|
||
|
||
it("keeps historical case keys distinct from ordinary task-document keys", () => {
|
||
const tool = definitions.find(definition => definition.name === "read_case")!;
|
||
for (const documentKey of ["Research:v1.notes", "a".repeat(120)]) {
|
||
expect(ajv.validate(tool.inputSchema as object, { section: "document", documentKey }), JSON.stringify(ajv.errors)).toBe(true);
|
||
}
|
||
});
|
||
});
|