Files
PaperClipAI/server/src/services/native-runtime/task-document-key-contract.test.ts
T
2026-11-03 12:31:17 -06:00

90 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
});
});