diff --git a/package.json b/package.json index a539789..c2a8117 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "tar": "^7.5.1", "tsx": "^4.23.15", "typebox": "1.3.7", - "zod": "^4.6.5" + "zod": "^4.6.6" }, "devDependencies": { "@types/node": "^24.19.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 847c9ed..be352ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,10 +10,10 @@ importers: dependencies: '@earendil-works/pi-agent-core': specifier: 0.83.0 - version: 0.83.0(ws@8.22.0)(zod@4.6.5) + version: 0.83.0(ws@8.22.0)(zod@4.6.6) '@earendil-works/pi-ai': specifier: 0.83.0 - version: 0.83.0(ws@8.22.0)(zod@4.6.5) + version: 0.83.0(ws@8.22.0)(zod@4.6.6) '@hono/node-server': specifier: ^2.1.0 version: 2.1.3(hono@4.13.13) @@ -36,8 +36,8 @@ importers: specifier: 1.3.7 version: 1.3.7 zod: - specifier: ^4.6.5 - version: 4.6.5 + specifier: ^4.6.6 + version: 4.6.6 devDependencies: '@types/node': specifier: ^24.19.0 @@ -1099,16 +1099,16 @@ packages: peerDependencies: zod: ^3.25.28 || ^4 - zod@4.6.5: - resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==} + zod@4.6.6: + resolution: {integrity: sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA==} snapshots: - '@anthropic-ai/sdk@0.91.1(zod@4.6.5)': + '@anthropic-ai/sdk@0.91.1(zod@4.6.6)': dependencies: json-schema-to-ts: 3.1.1 optionalDependencies: - zod: 4.6.5 + zod: 4.6.6 '@aws-crypto/sha256-browser@5.2.0': dependencies: @@ -1326,9 +1326,9 @@ snapshots: '@babel/runtime@7.29.7': {} - '@earendil-works/pi-agent-core@0.83.0(ws@8.22.0)(zod@4.6.5)': + '@earendil-works/pi-agent-core@0.83.0(ws@8.22.0)(zod@4.6.6)': dependencies: - '@earendil-works/pi-ai': 0.83.0(ws@8.22.0)(zod@4.6.5) + '@earendil-works/pi-ai': 0.83.0(ws@8.22.0)(zod@4.6.6) diff: 8.0.4 ignore: 7.0.5 typebox: 1.3.7 @@ -1341,9 +1341,9 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.83.0(ws@8.22.0)(zod@4.6.5)': + '@earendil-works/pi-ai@0.83.0(ws@8.22.0)(zod@4.6.6)': dependencies: - '@anthropic-ai/sdk': 0.91.1(zod@4.6.5) + '@anthropic-ai/sdk': 0.91.1(zod@4.6.6) '@aws-sdk/client-bedrock-runtime': 3.1048.0 '@google/genai': 1.52.0 '@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0) @@ -1351,7 +1351,7 @@ snapshots: '@smithy/node-http-handler': 4.7.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 - openai: 6.26.0(ws@8.22.0)(zod@4.6.5) + openai: 6.26.0(ws@8.22.0)(zod@4.6.6) partial-json: 0.1.7 typebox: 1.3.7 transitivePeerDependencies: @@ -1472,8 +1472,8 @@ snapshots: dependencies: '@opentelemetry/semantic-conventions': 1.43.0 ws: 8.22.0 - zod: 4.6.5 - zod-to-json-schema: 3.25.2(zod@4.6.5) + zod: 4.6.6 + zod-to-json-schema: 3.25.2(zod@4.6.6) optionalDependencies: '@opentelemetry/api': 1.9.0 transitivePeerDependencies: @@ -1901,10 +1901,10 @@ snapshots: obug@2.2.1: {} - openai@6.26.0(ws@8.22.0)(zod@4.6.5): + openai@6.26.0(ws@8.22.0)(zod@4.6.6): optionalDependencies: ws: 8.22.0 - zod: 4.6.5 + zod: 4.6.6 openapi-fetch@0.15.2: dependencies: @@ -2065,8 +2065,8 @@ snapshots: yaml@2.9.1: {} - zod-to-json-schema@3.25.2(zod@4.6.5): + zod-to-json-schema@3.25.2(zod@4.6.6): dependencies: - zod: 4.6.5 + zod: 4.6.6 - zod@4.6.5: {} + zod@4.6.6: {} diff --git a/test/zod-limits.test.ts b/test/zod-limits.test.ts new file mode 100644 index 0000000..d37d64d --- /dev/null +++ b/test/zod-limits.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import { MESSAGE_MAX_CHARS, Transcript, TRANSCRIPT_MAX_MESSAGES } from "../src/investigation.ts"; + +describe("Transcript validation limits", () => { + it("rejects an oversized transcript without allocating per-message validation issues", () => { + const input = Array.from({ length: 1_000 }, () => ({ role: "invalid", text: 42 })); + const parsed = Transcript.safeParse(input); + expect(parsed.success).toBe(false); + if (parsed.success) throw new Error("oversized transcript was accepted"); + + expect(parsed.error.issues).toHaveLength(1); + expect(parsed.error.issues[0]).toMatchObject({ code: "too_big", path: [], maximum: TRANSCRIPT_MAX_MESSAGES }); + }); + + it.each([1, TRANSCRIPT_MAX_MESSAGES])("accepts a valid transcript containing %i messages", (length) => { + const input = Array.from({ length }, () => ({ role: "user" as const, text: "x".repeat(MESSAGE_MAX_CHARS) })); + expect(Transcript.parse(input)).toEqual(input); + }); + + it("still rejects an empty transcript", () => { + const parsed = Transcript.safeParse([]); + expect(parsed.success).toBe(false); + if (parsed.success) throw new Error("empty transcript was accepted"); + expect(parsed.error.issues).toMatchObject([{ code: "too_small", path: [], minimum: 1 }]); + }); + + it("still validates individual messages within the transcript limit", () => { + const parsed = Transcript.safeParse([{ role: "invalid", text: 42 }]); + expect(parsed.success).toBe(false); + if (parsed.success) throw new Error("invalid message was accepted"); + expect(parsed.error.issues.map(({ path }) => path)).toEqual([[0, "role"], [0, "text"]]); + }); +});