Merge pull request #1003 from MBombeck/fix/cycle-symptom-severity-null

fix(cycle): accept a symptom logged without an intensity
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-09-18 19:33:22 +02:00
commit c183d1862e
3 files changed
+92 -11

No files matched your search

+15 -9
View File
@@ -20721,9 +20721,11 @@ components:
minLength: 1
maxLength: 80
severity:
type: integer
minimum: 1
maximum: 4
anyOf:
- type: integer
minimum: 1
maximum: 4
- type: "null"
required:
- key
note:
@@ -20859,9 +20861,11 @@ components:
minLength: 1
maxLength: 80
severity:
type: integer
minimum: 1
maximum: 4
anyOf:
- type: integer
minimum: 1
maximum: 4
- type: "null"
required:
- key
note:
@@ -20975,9 +20979,11 @@ components:
minLength: 1
maxLength: 80
severity:
type: integer
minimum: 1
maximum: 4
anyOf:
- type: integer
minimum: 1
maximum: 4
- type: "null"
required:
- key
note:
@@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";
import {
cycleDayLogInputSchema,
cycleDayLogPatchSchema,
} from "@/lib/validations/cycle";
/**
* A symptom picked without an intensity is the ordinary way to log one.
*
* The sheet holds the picker as key to intensity and sends `severity: null`
* for every symptom the user did not rate, which is most of them. The schema
* took a number or nothing at all, so those entries came back 422 and the
* sheet showed "Could not save. Try again." with no way to tell why. The link
* row has always stored an unrated symptom as a plain presence; only the
* gate at the door disagreed, and nothing tested it.
*/
const BASE_INPUT = {
date: "2026-09-17",
loggedAt: new Date().toISOString(),
} as const;
describe("cycle day-log symptoms accept an unrated selection", () => {
for (const [name, schema] of [
["create", cycleDayLogInputSchema],
["patch", cycleDayLogPatchSchema],
] as const) {
it(`${name}: takes a symptom with no intensity at all`, () => {
const parsed = schema.safeParse({
...BASE_INPUT,
symptoms: [{ key: "cramps" }],
});
expect(parsed.success, JSON.stringify(parsed.error?.issues)).toBe(true);
});
it(`${name}: takes an explicit null intensity, which is what the sheet sends`, () => {
const parsed = schema.safeParse({
...BASE_INPUT,
symptoms: [
{ key: "cramps", severity: null },
{ key: "back_pain", severity: null },
],
});
expect(parsed.success, JSON.stringify(parsed.error?.issues)).toBe(true);
});
it(`${name}: still takes a rated symptom`, () => {
const parsed = schema.safeParse({
...BASE_INPUT,
symptoms: [{ key: "cramps", severity: 3 }],
});
expect(parsed.success).toBe(true);
});
it(`${name}: still refuses an intensity outside 1 to 4`, () => {
for (const severity of [0, 5, 2.5]) {
const parsed = schema.safeParse({
...BASE_INPUT,
symptoms: [{ key: "cramps", severity }],
});
expect(parsed.success, `severity ${severity}`).toBe(false);
}
});
}
});
+12 -2
View File
@@ -131,7 +131,12 @@ export const cycleDayLogInputSchema = z.object({
.array(
z.object({
key: z.string().min(1).max(80),
severity: z.number().int().min(1).max(4).optional(),
// NULL is the ordinary case, not an edge: picking a symptom without
// tapping an intensity is how most entries are logged, and the link
// row stores that as a plain presence. Rejecting it refused every
// unrated symptom with a 422 the sheet could only show as
// "Could not save".
severity: z.number().int().min(1).max(4).nullable().optional(),
}),
)
.max(40)
@@ -181,7 +186,12 @@ export const cycleDayLogPatchSchema = z.object({
.array(
z.object({
key: z.string().min(1).max(80),
severity: z.number().int().min(1).max(4).optional(),
// NULL is the ordinary case, not an edge: picking a symptom without
// tapping an intensity is how most entries are logged, and the link
// row stores that as a plain presence. Rejecting it refused every
// unrated symptom with a 422 the sheet could only show as
// "Could not save".
severity: z.number().int().min(1).max(4).nullable().optional(),
}),
)
.max(40)