From 36ac116dc220223190b2500445665f86faf1a97c Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sat, 5 Sep 2026 16:04:10 +0200 Subject: [PATCH] fix(housekeeping): preserve radio key compatibility --- .../services/mutations-runtime-radio.test.ts | 56 ++++++++++++++++++- .../hotel/services/mutations-runtime.ts | 4 +- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/features/housekeeping/domains/hotel/services/mutations-runtime-radio.test.ts b/src/features/housekeeping/domains/hotel/services/mutations-runtime-radio.test.ts index 14d5924e..901788ca 100644 --- a/src/features/housekeeping/domains/hotel/services/mutations-runtime-radio.test.ts +++ b/src/features/housekeeping/domains/hotel/services/mutations-runtime-radio.test.ts @@ -1,3 +1,4 @@ +import { readFileSync } from "node:fs"; import type { SQL } from "drizzle-orm"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { HousekeepingCapabilityContext } from "../../../foundation/contracts"; @@ -52,6 +53,24 @@ const context = { legacy: true, }; +function readCuratedRadioSettingKeys(): string[] { + const pagePath = new URL( + "../../../../../app/admin/radio/settings/page.tsx", + import.meta.url, + ); + const source = readFileSync(pagePath, "utf8"); + const groupsStart = source.indexOf("const GROUPS: Group[] = ["); + const groupsEnd = source.indexOf("const CURATED_KEYS", groupsStart); + if (groupsStart < 0 || groupsEnd < 0) { + throw new Error("Radio settings GROUPS source contract was not found"); + } + + return Array.from( + source.slice(groupsStart, groupsEnd).matchAll(/\bkey:\s*"([^"]+)"/g), + (match) => match[1], + ); +} + beforeEach(() => { state.rows = []; state.writes = []; @@ -69,6 +88,34 @@ describe("Hotel radio settings runtime", () => { expect(state.writes).toEqual([]); }); + it.each(["radio_custom-setting.v2", "auto_dj_vendor:key/variant"])( + "accepts an existing custom prefixed setting key: %s", + async (key) => { + await executeHotelMutationOperation( + "radio.settings.save-one", + { key, value: "configured" }, + context, + ); + expect(state.writes).toEqual([ + expect.objectContaining({ key, value: "configured" }), + ]); + }, + ); + + it.each(["Radio_name", "AUTO_DJ_mode"])( + "rejects an uppercase setting prefix: %s", + async (key) => { + await expect( + executeHotelMutationOperation( + "radio.settings.save-one", + { key, value: "configured" }, + context, + ), + ).rejects.toBeInstanceOf(HotelMutationFailure); + expect(state.writes).toEqual([]); + }, + ); + it("rejects duplicate and invalid bulk entries atomically", async () => { for (const entries of [ [ @@ -92,10 +139,13 @@ describe("Hotel radio settings runtime", () => { }); it("accepts the complete 102-entry curated form within the 500-entry bound", async () => { - const entries = Array.from({ length: 102 }, (_, index) => ({ - key: `radio_curated_${index}`, + const keys = readCuratedRadioSettingKeys(); + const entries = keys.map((key, index) => ({ + key, value: `value-${index}`, })); + expect(keys).toHaveLength(102); + expect(new Set(keys).size).toBe(102); await executeHotelMutationOperation( "radio.settings.save-many", @@ -103,7 +153,7 @@ describe("Hotel radio settings runtime", () => { context, ); - expect(state.writes).toHaveLength(102); + expect(state.writes.map(({ key }) => key)).toEqual(keys); }); it("rejects a bulk request above 500 entries", async () => { diff --git a/src/features/housekeeping/domains/hotel/services/mutations-runtime.ts b/src/features/housekeeping/domains/hotel/services/mutations-runtime.ts index 8dba17db..a7d0148a 100644 --- a/src/features/housekeeping/domains/hotel/services/mutations-runtime.ts +++ b/src/features/housekeeping/domains/hotel/services/mutations-runtime.ts @@ -127,8 +127,8 @@ const roomRuntimeSchema = z }) .strict(); -const settingKey = requiredText(255).refine((key) => - /^(?:radio_|auto_dj_)[a-z0-9_]+$/i.test(key), +const settingKey = requiredText(255).refine( + (key) => key.startsWith("radio_") || key.startsWith("auto_dj_"), ); const settingSchema = z .object({