mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 02:32:07 +01:00
fix(runner): align ordinary document key contracts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
367f29478d
commit
d621c84b0e
11 files changed
+148
-39
No files matched your search
@@ -1 +1 @@
|
||||
[{"annotations":{"exposure":"always","operationId":"get_task_context","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the active task and actor, including the exact approved Markdown revision when this issue has an accepted plan.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_task_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"get_task_history","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded comments on the active task.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":[],"type":"object"},"name":"get_task_history","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_documents","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List revisioned documents on the active task.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_documents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"read_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the current revision of one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"}},"required":["key"],"type":"object"},"name":"read_document","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_document_revisions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded revision history for one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"},"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":["key"],"type":"object"},"name":"list_document_revisions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"report_progress","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append a durable progress comment to the active task.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Multiline progress update.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"report_progress","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"answer_status_question","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append the answer to a status-only wake without changing task disposition.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Concise status answer.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"answer_status_question","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"write_document","requiredClaimLine truncated
|
||||
[{"annotations":{"exposure":"always","operationId":"get_task_context","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the active task and actor, including the exact approved Markdown revision when this issue has an accepted plan.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_task_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"get_task_history","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded comments on the active task.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":[],"type":"object"},"name":"get_task_history","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_documents","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List revisioned documents on the active task.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_documents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"read_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the current revision of one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable document identifier, not a filename or path (for example reconciliation, not reconciliation.md). After trimming whitespace and lowercasing: 1–64 characters, starting with a letter or digit and containing only a–z, 0–9, _ or -. The raw input is limited to 120 characters.","maxLength":120,"minLength":1,"pattern":"^[\\u0009-\\u000d\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]*[A-Za-z0-9\\u212a][A-Za-z0-9_\\u212a-]{0,63}[\\u0009-\\u000d\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]*$","type":"string"}},"required":["key"],"type":"object"},"name":"read_document","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_document_revisions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded revision history for one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable document identifier, not a filename or path (for example reconciliation, not reconciliation.md). After trimming whitespace and lowercasing: 1–64 characters, starting with a letter or digit and containing only a–z, 0–9, _ or -. The raw input is limited to 120 characters.","maxLength":120,"minLength":1,"pattern":"^[\\u0009-\\u000d\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]*[A-Za-z0-9\\u212a][A-Za-z0-9_\\u212a-]{0,63}[\\u0009-\\u000d\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]*$","type":"string"},"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":["key"],"type":"object"},"name":"list_document_revisions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"report_progress","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append a durable progress comment to the active task.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Multiline progress update.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"report_progress","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"answer_status_question","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append the answer to a status-only wake without changing task disposition.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Concise status answeLine truncated
|
||||
@@ -336,9 +336,10 @@
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": {
|
||||
"description": "Stable issue-document key.",
|
||||
"description": "Stable document identifier, not a filename or path (for example reconciliation, not reconciliation.md). After trimming whitespace and lowercasing: 1–64 characters, starting with a letter or digit and containing only a–z, 0–9, _ or -. The raw input is limited to 120 characters.",
|
||||
"maxLength": 120,
|
||||
"minLength": 1,
|
||||
"pattern": "^[\\u0009-\\u000d\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]*[A-Za-z0-9\\u212a][A-Za-z0-9_\\u212a-]{0,63}[\\u0009-\\u000d\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]*$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -371,9 +372,10 @@
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": {
|
||||
"description": "Stable issue-document key.",
|
||||
"description": "Stable document identifier, not a filename or path (for example reconciliation, not reconciliation.md). After trimming whitespace and lowercasing: 1–64 characters, starting with a letter or digit and containing only a–z, 0–9, _ or -. The raw input is limited to 120 characters.",
|
||||
"maxLength": 120,
|
||||
"minLength": 1,
|
||||
"pattern": "^[\\u0009-\\u000d\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]*[A-Za-z0-9\\u212a][A-Za-z0-9_\\u212a-]{0,63}[\\u0009-\\u000d\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]*$",
|
||||
"type": "string"
|
||||
},
|
||||
"limit": {
|
||||
@@ -699,9 +701,10 @@
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"description": "Stable issue-document key.",
|
||||
"description": "Stable document identifier, not a filename or path (for example reconciliation, not reconciliation.md). After trimming whitespace and lowercasing: 1–64 characters, starting with a letter or digit and containing only a–z, 0–9, _ or -. The raw input is limited to 120 characters.",
|
||||
"maxLength": 120,
|
||||
"minLength": 1,
|
||||
"pattern": "^[\\u0009-\\u000d\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]*[A-Za-z0-9\\u212a][A-Za-z0-9_\\u212a-]{0,63}[\\u0009-\\u000d\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]*$",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"prpVersion": 1,
|
||||
"nativeExecutionVersion": 1,
|
||||
"catalogVersion": 1,
|
||||
"catalogSha256": "sha256:d1886239c2dc902879b3e1738ca39c7efe93a39a3ece937a7390f720e5bda18c",
|
||||
"catalogSha256": "sha256:ea55aec60ac7737359115227cc0a5e9000bf0098d1e3f92d4281661cac290868",
|
||||
"driverContractVersion": 1,
|
||||
"driverKind": "paperclip-deterministic",
|
||||
"driverVersion": "1.0.0"
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
},
|
||||
{
|
||||
"path": "fixtures/evals/native-execution-seeded.json",
|
||||
"sha256": "319fdf4c80abbd24073c4f1bd041d4b183e6e4660bd473762c9765007c479ae0",
|
||||
"sha256": "5ce8623132b60eb3e38214ebb75b98172c8a5cf177791a7a43ef75fe66c89db5",
|
||||
"expectation": "accept",
|
||||
"compatibilityCase": "canonical"
|
||||
},
|
||||
|
||||
@@ -6,6 +6,9 @@ import Ajv2020 from "ajv/dist/2020.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createTaskAction } from "../protocol-actions/create-task.js";
|
||||
import { createProjectAction } from "../protocol-actions/create-project.js";
|
||||
import { readDocumentAction } from "../protocol-actions/read-document.js";
|
||||
import { writeDocumentAction } from "../protocol-actions/write-document.js";
|
||||
import { listDocumentRevisionsAction } from "../protocol-actions/list-document-revisions.js";
|
||||
import { handoffCaseAction } from "../protocol-actions/handoff-case.js";
|
||||
|
||||
import {
|
||||
@@ -21,6 +24,26 @@ const packageRoot = resolve(
|
||||
);
|
||||
|
||||
describe("semantic action catalog", () => {
|
||||
it.each([readDocumentAction, writeDocumentAction, listDocumentRevisionsAction])(
|
||||
"$id uses the same normalized key contract on every tool surface",
|
||||
(action) => {
|
||||
const ajv = new Ajv2020({ strict: true });
|
||||
const schemas = [
|
||||
action.live.descriptor.inputSchema.properties.key,
|
||||
action.scenario.descriptor.inputSchema.properties.key,
|
||||
paperclipSemanticAction(action.id)!.inputSchema.properties!.key,
|
||||
];
|
||||
for (const schema of schemas) {
|
||||
const validate = ajv.compile(schema);
|
||||
for (const key of ["reconciliation", "Reconciliation", "K", " ".repeat(56) + "a".repeat(64)]) {
|
||||
expect(validate(key), key).toBe(true);
|
||||
}
|
||||
for (const key of ["reconciliation.md", "../document", "a".repeat(65), " ".repeat(57) + "a".repeat(64)]) {
|
||||
expect(validate(key), key).toBe(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
it("compiles case handoff identity constraints in strict consumers and rejects malformed IDs", () => {
|
||||
const ajv = new Ajv2020({ allErrors: true, allowUnionTypes: true, strict: true });
|
||||
for (const schema of [handoffCaseAction.live.descriptor.inputSchema, paperclipSemanticAction("handoff_case")!.inputSchema]) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { taskDocumentKeySchema } from "../protocol-actions/task-document-key-schema.js";
|
||||
import { getWorkspaceAction } from "../protocol-actions/get-workspace.js";
|
||||
import { handoffCaseAction } from "../protocol-actions/handoff-case.js";
|
||||
import { createCaseAction, readCaseAction, updateCaseAction } from "../protocol-actions/case-data.js";
|
||||
@@ -156,7 +157,7 @@ const descriptors: readonly PaperclipSemanticActionDescriptor[] = [
|
||||
operationId: "read_document",
|
||||
title: "Read task document",
|
||||
description: "Read the current revision of one active-task document.",
|
||||
inputSchema: object({ key: text("Stable issue-document key.", 120) }, [
|
||||
inputSchema: object({ key: taskDocumentKeySchema }, [
|
||||
"key",
|
||||
]),
|
||||
}),
|
||||
@@ -166,7 +167,7 @@ const descriptors: readonly PaperclipSemanticActionDescriptor[] = [
|
||||
description: "Read bounded revision history for one active-task document.",
|
||||
inputSchema: object(
|
||||
{
|
||||
key: text("Stable issue-document key.", 120),
|
||||
key: taskDocumentKeySchema,
|
||||
limit: { type: "integer", minimum: 1, maximum: 200, default: 50 },
|
||||
},
|
||||
["key"],
|
||||
@@ -220,7 +221,7 @@ const descriptors: readonly PaperclipSemanticActionDescriptor[] = [
|
||||
inputSchema: object(
|
||||
{
|
||||
...idempotency,
|
||||
key: text("Stable issue-document key.", 120),
|
||||
key: taskDocumentKeySchema,
|
||||
title: text("Document title.", 300),
|
||||
body: text("Markdown document body.", 200_000),
|
||||
baseRevisionId: nullableText(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { taskDocumentKeySchema } from "./task-document-key-schema.js";
|
||||
|
||||
/** Canonical definition and documentation for `list_document_revisions`. */
|
||||
export const listDocumentRevisionsAction = {
|
||||
"id": "list_document_revisions",
|
||||
@@ -62,12 +64,7 @@ export const listDocumentRevisionsAction = {
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "Stable issue-document key.",
|
||||
"minLength": 1,
|
||||
"maxLength": 120
|
||||
},
|
||||
"key": taskDocumentKeySchema,
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
@@ -105,10 +102,7 @@ export const listDocumentRevisionsAction = {
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
"key": taskDocumentKeySchema
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { taskDocumentKeySchema } from "./task-document-key-schema.js";
|
||||
|
||||
/** Canonical definition and documentation for `read_document`. */
|
||||
export const readDocumentAction = {
|
||||
"id": "read_document",
|
||||
@@ -62,12 +64,7 @@ export const readDocumentAction = {
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "Stable issue-document key.",
|
||||
"minLength": 1,
|
||||
"maxLength": 120
|
||||
}
|
||||
"key": taskDocumentKeySchema
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
@@ -99,10 +96,7 @@ export const readDocumentAction = {
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
"key": taskDocumentKeySchema
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// Matches documents.ts trim().toLowerCase() followed by issueDocumentKeySchema.
|
||||
// U+212A (Kelvin sign) lowercases to ASCII k; the whitespace set is ECMAScript trim.
|
||||
const trimWhitespace = String.raw`[\u0009-\u000d\u0020\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]*`;
|
||||
|
||||
export const taskDocumentKeySchema = {
|
||||
type: "string",
|
||||
description: "Stable document identifier, not a filename or path (for example reconciliation, not reconciliation.md). After trimming whitespace and lowercasing: 1–64 characters, starting with a letter or digit and containing only a–z, 0–9, _ or -. The raw input is limited to 120 characters.",
|
||||
minLength: 1,
|
||||
maxLength: 120,
|
||||
pattern: `^${trimWhitespace}[A-Za-z0-9\\u212a][A-Za-z0-9_\\u212a-]{0,63}${trimWhitespace}$`,
|
||||
} as const;
|
||||
@@ -1,3 +1,5 @@
|
||||
import { taskDocumentKeySchema } from "./task-document-key-schema.js";
|
||||
|
||||
/** Canonical definition and documentation for `write_document`. */
|
||||
export const writeDocumentAction = {
|
||||
"id": "write_document",
|
||||
@@ -90,12 +92,7 @@ export const writeDocumentAction = {
|
||||
"minLength": 1,
|
||||
"maxLength": 240
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "Stable issue-document key.",
|
||||
"minLength": 1,
|
||||
"maxLength": 120
|
||||
},
|
||||
"key": taskDocumentKeySchema,
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Document title.",
|
||||
@@ -210,10 +207,7 @@ export const writeDocumentAction = {
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"key": taskDocumentKeySchema,
|
||||
"title": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user