diff --git a/pkg/service/marge/issue253_regression_test.go b/pkg/service/marge/issue253_regression_test.go
new file mode 100644
index 0000000..852df0f
--- /dev/null
+++ b/pkg/service/marge/issue253_regression_test.go
@@ -0,0 +1,136 @@
+package marge
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/gesellix/bose-soundtouch/pkg/service/datastore"
+)
+
+// TestIssue253_PresetsXMLEditPropagatesToMargeResponse documents the
+// service-side half of issue #253:
+//
+// https://github.com/gesellix/Bose-SoundTouch/issues/253
+//
+// The reporter edits AfterTouch's persisted Presets.xml on disk and
+// expects the change to show up on the speaker's :8090/presets. That
+// propagation chain has three links:
+//
+// 1. disk → marge: AfterTouch's marge serves the edited XML when the
+// speaker GETs /streaming/account/.../device/.../presets (or
+// /full). This is the link this test exercises.
+// 2. marge → device: the speaker has to re-fetch (typically nudged by
+// a /streaming/support/power_on or by a sourcesUpdated
+// notification — not exercised here, that's a runbook concern).
+// 3. device → :8090: once the device's local cache updates, its
+// /presets endpoint reflects. Out of our reach.
+//
+// If link (1) is broken — e.g. marge caches the rendered XML between
+// requests, or ds.GetPresets returns stale data — neither (2) nor (3)
+// can recover, and the reporter's symptom is inevitable. This test
+// proves (1) is sound by:
+//
+// - Writing testdata/issue253/presets_v1.xml directly into the
+// datastore (no SavePresets — the reporter is editing on disk).
+// - Calling PresetsToXML, asserting v1's itemName and location land
+// in the rendered response.
+// - Overwriting the file with testdata/issue253/presets_v2.xml.
+// - Calling PresetsToXML again, asserting v2's itemName and
+// location land — and v1's are gone.
+//
+// If link (1) ever regresses (a caching layer added without
+// invalidation, a fs handle held open across edits, …), this test
+// fails on the second assertion. When that happens, fix the
+// invalidation rather than weakening the test.
+//
+// Pattern mirrors recents_sourceproviderid_regression_test.go: write
+// XML directly to the datastore filesystem, exercise the marge
+// function the handler uses (PresetsToXML at marge.go:370), assert on
+// the rendered bytes.
+func TestIssue253_PresetsXMLEditPropagatesToMargeResponse(t *testing.T) {
+ v1, err := os.ReadFile(filepath.Join("testdata", "issue253", "presets_v1.xml"))
+ if err != nil {
+ t.Fatalf("read v1 fixture: %v", err)
+ }
+
+ v2, err := os.ReadFile(filepath.Join("testdata", "issue253", "presets_v2.xml"))
+ if err != nil {
+ t.Fatalf("read v2 fixture: %v", err)
+ }
+
+ // Fixture sanity — a typo in testdata would silently invalidate
+ // the assertions below.
+ if !strings.Contains(string(v1), "Initial Station") ||
+ !strings.Contains(string(v1), "sINITIAL") {
+ t.Fatalf("v1 fixture missing expected markers; got:\n%s", v1)
+ }
+
+ if !strings.Contains(string(v2), "Edited Station") ||
+ !strings.Contains(string(v2), "sEDITED") {
+ t.Fatalf("v2 fixture missing expected markers; got:\n%s", v2)
+ }
+
+ tempDir, err := os.MkdirTemp("", "issue253-*")
+ if err != nil {
+ t.Fatalf("mkdir temp: %v", err)
+ }
+
+ t.Cleanup(func() { _ = os.RemoveAll(tempDir) })
+
+ const (
+ account = "issue253"
+ deviceID = "DEADBEEFCAFE"
+ )
+
+ deviceDir := filepath.Join(tempDir, "accounts", account, "devices", deviceID)
+ if err := os.MkdirAll(deviceDir, 0o755); err != nil {
+ t.Fatalf("mkdir device dir: %v", err)
+ }
+
+ presetsPath := filepath.Join(deviceDir, "Presets.xml")
+
+ ds := datastore.NewDataStore(tempDir)
+
+ // First render: write v1 to disk, ask marge for the wire bytes.
+ if err := os.WriteFile(presetsPath, v1, 0o644); err != nil {
+ t.Fatalf("write v1: %v", err)
+ }
+
+ render1, err := PresetsToXML(ds, account, deviceID)
+ if err != nil {
+ t.Fatalf("PresetsToXML (v1): %v", err)
+ }
+
+ if !strings.Contains(string(render1), "Initial Station") {
+ t.Errorf("v1 render missing 'Initial Station'; body:\n%s", render1)
+ }
+
+ if !strings.Contains(string(render1), "/v1/playback/station/sINITIAL") {
+ t.Errorf("v1 render missing initial location; body:\n%s", render1)
+ }
+
+ // Second render after on-disk edit: must reflect v2, not v1.
+ if err := os.WriteFile(presetsPath, v2, 0o644); err != nil {
+ t.Fatalf("write v2: %v", err)
+ }
+
+ render2, err := PresetsToXML(ds, account, deviceID)
+ if err != nil {
+ t.Fatalf("PresetsToXML (v2): %v", err)
+ }
+
+ if !strings.Contains(string(render2), "Edited Station") {
+ t.Errorf("v2 render missing 'Edited Station' — disk edit did not propagate. Likely a caching layer added between requests; render body:\n%s", render2)
+ }
+
+ if !strings.Contains(string(render2), "/v1/playback/station/sEDITED") {
+ t.Errorf("v2 render missing edited location; body:\n%s", render2)
+ }
+
+ if strings.Contains(string(render2), "Initial Station") ||
+ strings.Contains(string(render2), "sINITIAL") {
+ t.Errorf("v2 render still carries v1 content — propagation broken. Body:\n%s", render2)
+ }
+}
diff --git a/pkg/service/marge/testdata/issue253/presets_v1.xml b/pkg/service/marge/testdata/issue253/presets_v1.xml
new file mode 100644
index 0000000..54b427d
--- /dev/null
+++ b/pkg/service/marge/testdata/issue253/presets_v1.xml
@@ -0,0 +1,9 @@
+
+
+
+
+ Initial Station
+ https://example.invalid/initial.jpg
+
+
+
diff --git a/pkg/service/marge/testdata/issue253/presets_v2.xml b/pkg/service/marge/testdata/issue253/presets_v2.xml
new file mode 100644
index 0000000..13dc65c
--- /dev/null
+++ b/pkg/service/marge/testdata/issue253/presets_v2.xml
@@ -0,0 +1,9 @@
+
+
+
+
+ Edited Station
+ https://example.invalid/edited.jpg
+
+
+