fix: bound conversational nested JSON arrays
This commit is contained in:
@@ -66,8 +66,8 @@ const candidateSchema = boundedJson(z.object({
|
||||
|
||||
const technicalReceiptSchema = boundedJson(z.object({
|
||||
calculationVersion: boundedNonblankText(80),
|
||||
stableLayers: z.array(boundedNonblankText(80)).max(20),
|
||||
sensitiveLayers: z.array(boundedNonblankText(80)).max(20),
|
||||
stableLayers: boundedJson(z.array(boundedNonblankText(80)).max(20), 4_096),
|
||||
sensitiveLayers: boundedJson(z.array(boundedNonblankText(80)).max(20), 4_096),
|
||||
candidateDifferenceRefs: z.array(boundedNonblankText(120)).max(40),
|
||||
}).strict(), 8_192);
|
||||
|
||||
|
||||
@@ -152,10 +152,10 @@ export const privateCandidateSchema = boundedJson(z.object({
|
||||
rangeEnd: timeSchema.nullable().optional(),
|
||||
calculationVersion: boundedText(80),
|
||||
candidateWeights: z.array(z.number().finite().min(0).max(1)).max(1_440).optional(),
|
||||
candidateModelRefs: z.array(boundedText(120)).max(80).optional(),
|
||||
candidateModelRefs: boundedJson(z.array(boundedText(120)).max(80), 16_384).optional(),
|
||||
d1Stability: z.enum(["stable", "sensitive", "unavailable"]).optional(),
|
||||
boundaryDistanceMinutes: z.number().int().min(0).max(1_440).nullable().optional(),
|
||||
supportedSensitiveLayers: z.array(boundedText(80)).max(40).optional(),
|
||||
supportedSensitiveLayers: boundedJson(z.array(boundedText(80)).max(40), 8_192).optional(),
|
||||
scoredHistoricalEvidence: z.array(scoredEvidenceSchema).max(100).optional(),
|
||||
suggestedDomains: z.array(evidenceDomainSchema).max(6).optional(),
|
||||
futureWindows: z.array(futureWindowSchema).max(20).optional(),
|
||||
|
||||
@@ -616,6 +616,73 @@ test("evidence recap enforces the SQL-matched aggregate byte limit", () => {
|
||||
assert.equal(conversationalRectificationTurnSchema.safeParse(turn).success, false);
|
||||
});
|
||||
|
||||
test("nested receipt and private-candidate arrays match SQL UTF-8 byte caps", () => {
|
||||
const fullLayer = "事".repeat(80);
|
||||
const receiptAtLimit = [
|
||||
...Array.from({ length: 16 }, () => fullLayer),
|
||||
`${"事".repeat(62)}aa`,
|
||||
];
|
||||
const receiptOverLimit = [
|
||||
...Array.from({ length: 16 }, () => fullLayer),
|
||||
"事".repeat(61),
|
||||
"aa",
|
||||
];
|
||||
assert.equal(postgresJsonbTextBytes(receiptAtLimit), 4_096);
|
||||
assert.equal(postgresJsonbTextBytes(receiptOverLimit), 4_097);
|
||||
|
||||
for (const field of ["stableLayers", "sensitiveLayers"] as const) {
|
||||
assert.equal(conversationalRectificationTurnSchema.safeParse({
|
||||
...firstTurn,
|
||||
technicalReceipt: { ...firstTurn.technicalReceipt, [field]: receiptAtLimit },
|
||||
}).success, true, `${field} at its SQL byte limit`);
|
||||
assert.equal(conversationalRectificationTurnSchema.safeParse({
|
||||
...firstTurn,
|
||||
technicalReceipt: { ...firstTurn.technicalReceipt, [field]: receiptOverLimit },
|
||||
}).success, false, `${field} above its SQL byte limit`);
|
||||
}
|
||||
|
||||
const fullModelReference = "事".repeat(120);
|
||||
const modelReferencesAtLimit = [
|
||||
...Array.from({ length: 44 }, () => fullModelReference),
|
||||
"事".repeat(119),
|
||||
"aaa",
|
||||
];
|
||||
const modelReferencesOverLimit = [
|
||||
...Array.from({ length: 44 }, () => fullModelReference),
|
||||
"事".repeat(119),
|
||||
"aaaa",
|
||||
];
|
||||
assert.equal(postgresJsonbTextBytes(modelReferencesAtLimit), 16_384);
|
||||
assert.equal(postgresJsonbTextBytes(modelReferencesOverLimit), 16_385);
|
||||
|
||||
const supportedLayersAtLimit = [
|
||||
...Array.from({ length: 33 }, () => fullLayer),
|
||||
`${"事".repeat(45)}a`,
|
||||
];
|
||||
const supportedLayersOverLimit = [
|
||||
...Array.from({ length: 33 }, () => fullLayer),
|
||||
`${"事".repeat(45)}aa`,
|
||||
];
|
||||
assert.equal(postgresJsonbTextBytes(supportedLayersAtLimit), 8_192);
|
||||
assert.equal(postgresJsonbTextBytes(supportedLayersOverLimit), 8_193);
|
||||
|
||||
for (const [field, atLimit, overLimit] of [
|
||||
["candidateModelRefs", modelReferencesAtLimit, modelReferencesOverLimit],
|
||||
["supportedSensitiveLayers", supportedLayersAtLimit, supportedLayersOverLimit],
|
||||
] as const) {
|
||||
assert.equal(privateCandidateSchema.safeParse({
|
||||
resultId,
|
||||
calculationVersion: "rectification-v3.1",
|
||||
[field]: atLimit,
|
||||
}).success, true, `${field} at its SQL byte limit`);
|
||||
assert.equal(privateCandidateSchema.safeParse({
|
||||
resultId,
|
||||
calculationVersion: "rectification-v3.1",
|
||||
[field]: overLimit,
|
||||
}).success, false, `${field} above its SQL byte limit`);
|
||||
}
|
||||
});
|
||||
|
||||
test("life-event evidence rejects unknown, blank, and non-boolean durable values", () => {
|
||||
const evidence = {
|
||||
id: "00000000-0000-4000-8000-000000000105",
|
||||
|
||||
Reference in New Issue
Block a user