fix subroom save description clobbering room description

This commit is contained in:
Devin Zuczek
2026-08-06 15:35:35 -04:00
parent 565d9b1aea
commit 51364e482c
5 changed files with 62 additions and 24 deletions
+11 -4
View File
@@ -238,6 +238,7 @@ export const SubRoomDto = z.object({
RoomDataBlob: z.string().optional().describe('Uploaded room-data key; absent until first save'),
DataSavedAt: z.string().optional().describe('ISO timestamp of the last save'),
PersistenceVersion: z.int().optional(),
InventionUsage: z.string().optional().describe('Recorded by a room save; absent until then'),
})
/** A room's localization settings — carried through verbatim; nothing localizes yet. */
@@ -310,7 +311,10 @@ export const RoomDto = z.object({
PromoExternalContent: z.array(z.unknown()),
LoadScreens: z.array(LoadScreenDto),
RestrictedCircuitsAllowListNames: z.array(z.string()),
InventionUsage: z.string().optional().describe('Recorded by a room save; absent until then'),
InventionUsage: z
.string()
.optional()
.describe('Legacy: room saves used to write this here; it now lives on the SUBROOM'),
})
/** A paged room list (`PagedResultsDTO<RoomDTO>`) — search, hot, similar. */
@@ -598,9 +602,12 @@ export const SaveSubRoomDataRequest = z.object({
.object({ Filename: z.string() })
.optional()
.describe('The uploaded room-level data blob — becomes `RoomDataBlob`'),
Description: z.string().optional().describe('The save comment; also written to the ROOM'),
PersistenceVersion: z.int().optional(),
InventionUsage: z.string().optional().describe('Written to the room'),
Description: z
.string()
.optional()
.describe('The save comment — a description of THIS revision, not the rooms description'),
PersistenceVersion: z.int().optional().describe('Recorded on the save and the subroom'),
InventionUsage: z.string().optional().describe('Recorded on the subroom'),
UnityAssetId: z.string().nullable().optional().describe('Recorded on the save when set'),
AutoPublish: z
.boolean()
+8 -7
View File
@@ -1931,9 +1931,9 @@ const app = new Hono<App>()
// Save a subroom's data (room save). Auth-gated (401 with empty body). Editable
// by the room creator or a Creator/CoOwner role holder. Points the subroom at
// the uploaded data blobs and records the room-level save fields, notifies the
// owner, and returns the updated ROOM in the lowercase `{ success, error, value }`
// envelope the reference's SetRoomData uses.
// the uploaded data blobs and records the revision's fields against that SUBROOM,
// notifies the owner, and returns the updated ROOM in the lowercase
// `{ success, error, value }` envelope the reference's SetRoomData uses.
.post(
'/rooms/:roomId{[0-9]+}/subrooms/:subRoomId{[0-9]+}/data',
describeRoute({
@@ -1941,10 +1941,11 @@ const app = new Hono<App>()
summary: 'Save a subrooms data (room save)',
description: [
'Records a save against the subroom from the blobs the client has already uploaded',
'through the `storage` worker; the room-level fields it carries (`Description`,',
'`PersistenceVersion`, `InventionUsage`) are written to the room. Editable by the',
'rooms creator or a co-owner (403 otherwise); a missing token is an EMPTY-body 401,',
'unlike the other room writes.',
'through the `storage` worker. Everything the body carries describes THAT revision',
'and lands on the save and its subroom — `Description` is the save comment, NOT the',
'rooms description (only `PUT /rooms/{roomId}/description` sets that). Nothing here',
'writes to the room. Editable by the rooms creator or a co-owner (403 otherwise); a',
'missing token is an EMPTY-body 401, unlike the other room writes.',
'',
'`AutoPublish: true` makes the save live immediately. Otherwise it is STAGED: it',
'lands on `StagedSubRoomDataSaveId` with the live `CurrentSave` untouched, so',
+19 -4
View File
@@ -1376,6 +1376,15 @@ describe('rooms endpoints', () => {
})
expect(await envOf(await authed(2, 9999, '1'))).toMatchObject({ success: false })
// The room's own fields are read first: a save is a revision of a SUBROOM and must
// leave them alone. `Description` in the body is the save comment, not the room's
// description — that is `PUT /rooms/:id/description`'s to set.
const before = (await (await SELF.fetch(`${ORIGIN}/rooms/2`)).json()) as {
Description: string
PersistenceVersion: number
}
expect(before.Description).not.toBe('mydescription here')
// Owner saves → 200. `value` carries BOTH the updated room and the new save, and
// `error` is null (not ''). This fixture sends `AutoPublish: true`, so it goes live.
const ok = await authed(2, 2, '1')
@@ -1390,7 +1399,7 @@ describe('rooms endpoints', () => {
}
expect(saved.success).toBe(true)
expect(saved.error).toBeNull()
expect(saved.value.room).toMatchObject({ RoomId: 2, Description: 'mydescription here' })
expect(saved.value.room).toMatchObject({ RoomId: 2, Description: before.Description })
// The save is a camelCase projection, NOT the PascalCase CurrentSave shape.
expect(saved.value.subRoomDataSave).toEqual({
@@ -1430,6 +1439,7 @@ describe('rooms endpoints', () => {
SubRoomDataSaveId: number
SavedByAccountId: number
PersistenceVersion: number
Description: string
UnitySubAssets: unknown[]
Tags: unknown[]
}
@@ -1446,13 +1456,18 @@ describe('rooms endpoints', () => {
expect(sub.CurrentSave.SubRoomDataSaveId).toBeGreaterThan(0)
expect(sub.StagedSubRoomDataSaveId).toBeNull()
// Room-level fields land on the room too.
// The save comment and the scene fields land on the SUBROOM's revision, and the
// room's own fields are untouched — a save must never rewrite the room.
expect(sub.CurrentSave.Description).toBe('mydescription here')
expect(sub).toMatchObject({ PersistenceVersion: 41, InventionUsage: 'CAE=' })
const room = (await (await SELF.fetch(`${ORIGIN}/rooms/2`)).json()) as {
Description: string
PersistenceVersion: number
InventionUsage?: string
}
expect(room.Description).toBe('mydescription here')
expect(room.PersistenceVersion).toBe(41)
expect(room.Description).toBe(before.Description)
expect(room.PersistenceVersion).toBe(before.PersistenceVersion)
expect(room.InventionUsage).toBeUndefined()
// A CoOwner (account 2 holds Role 30 in the seeded rooms) may also save — 200
// with the room envelope. The creator stays account 1 (not clobbered).