Serialize Web preparation commands strictly

This commit is contained in:
fy59 2026-09-18 05:29:23 +02:00
parent f57682790d
commit 9cdd9c2313
2 changed files with 54 additions and 5 deletions

View file

@ -1,6 +1,6 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { SessionsPage } from './SessionsPage'
import { commandOccurrence, SessionsPage } from './SessionsPage'
function page(kind: string, items: unknown[]) {
return { api_version: 1, kind, offset: 0, more: false, next_offset: items.length, items }
@ -11,6 +11,19 @@ function response(value: unknown) {
}
describe('Sessions page', () => {
it('removes presentation-only fields from a preparation command', () => {
const command = commandOccurrence({
exercise_id: 'ex_one', exercise_name: 'Visible name', equipment_id: null,
recording_mode: 'sets', tracking_mode: 'reps', load_mode: 'external', rest_seconds: 90,
target_sets: 3, target_reps: 8, target_duration_seconds: null, target_weight_kg: 42,
notes: null,
})
expect(command).not.toHaveProperty('exercise_name')
expect(command).not.toHaveProperty('recording_mode')
expect(command).not.toHaveProperty('tracking_mode')
expect(command).toMatchObject({ exercise_id: 'ex_one', target_sets: 3, rest_seconds: 90 })
})
it('keeps proposals distinct and opens a stable deep link', async () => {
window.history.replaceState(null, '', '/seances')
vi.stubGlobal('fetch', vi.fn((input: RequestInfo | URL) => {

View file

@ -31,6 +31,42 @@ function emptyInput(): PreparationInput {
editing_state: 'draft', occurrences: [] }
}
function editableOccurrence(
value: PreparationOccurrence,
preserveIdentity: boolean,
): PreparationOccurrence {
return {
...(preserveIdentity ? { entry_id: value.entry_id } : {}),
exercise_id: value.exercise_id,
exercise_name: value.exercise_name,
equipment_id: value.equipment_id,
recording_mode: value.recording_mode,
tracking_mode: value.tracking_mode,
load_mode: value.load_mode,
rest_seconds: value.rest_seconds,
target_sets: value.target_sets,
target_reps: value.target_reps,
target_duration_seconds: value.target_duration_seconds,
target_weight_kg: value.target_weight_kg,
notes: value.notes,
}
}
export function commandOccurrence(value: PreparationOccurrence): PreparationOccurrence {
return {
...(value.entry_id ? { entry_id: value.entry_id } : {}),
exercise_id: value.exercise_id,
equipment_id: value.equipment_id,
load_mode: value.load_mode,
rest_seconds: value.rest_seconds,
target_sets: value.target_sets,
target_reps: value.target_reps,
target_duration_seconds: value.target_duration_seconds,
target_weight_kg: value.target_weight_kg,
notes: value.notes,
}
}
function Editor({ initial, onSaved, onCancel }: {
initial?: SessionDetail
onSaved: (identity: string) => void
@ -38,9 +74,8 @@ function Editor({ initial, onSaved, onCancel }: {
}) {
const [input, setInput] = useState<PreparationInput>(() => initial ? {
title: initial.title, session_type: initial.session_type, planned_for: initial.planned_for,
notes: initial.notes, editing_state: 'draft', occurrences: initial.occurrences.map((value) => ({
...value, entry_id: initial.kind === 'proposal' ? undefined : value.entry_id,
})),
notes: initial.notes, editing_state: 'draft', occurrences: initial.occurrences.map((value) =>
editableOccurrence(value, initial.kind === 'preparation')),
...(initial.kind === 'proposal' ? {
source_proposal_id: initial.identity,
source_payload_sha256: initial.source_fingerprint ?? undefined,
@ -78,7 +113,8 @@ function Editor({ initial, onSaved, onCancel }: {
const submit = async (ready: boolean) => {
setSaving(true); setError('')
try {
const result = await savePreparation({ ...input, editing_state: ready ? 'ready' : 'draft' },
const result = await savePreparation({ ...input, editing_state: ready ? 'ready' : 'draft',
occurrences: input.occurrences.map(commandOccurrence) },
initial?.kind === 'preparation' ? initial.identity : undefined,
initial?.kind === 'preparation' ? initial.revision_id : undefined)
if (ready) await prepareForAndroid(result.preparation_id, result.revision_id)