route_update_test.go

  1package waypoint
  2
  3import (
  4	"encoding/json"
  5	"testing"
  6
  7	"git.kilimanjaro.io/ygo"
  8)
  9
 10func TestUpdateWaypointRoutes_NoUpdates(t *testing.T) {
 11	doc, err := ygo.NewDoc()
 12	if err != nil {
 13		t.Fatalf("failed to create doc: %v", err)
 14	}
 15	defer doc.Destroy()
 16
 17	// No updates - should return nil
 18	err = UpdateWaypointRoutes(doc, nil)
 19	if err != nil {
 20		t.Fatalf("unexpected error: %v", err)
 21	}
 22
 23	// Empty slice - should return nil
 24	err = UpdateWaypointRoutes(doc, []RouteUpdate{})
 25	if err != nil {
 26		t.Fatalf("unexpected error: %v", err)
 27	}
 28}
 29
 30func TestUpdateWaypointRoutes_SingleWaypoint(t *testing.T) {
 31	doc, err := ygo.NewDoc()
 32	if err != nil {
 33		t.Fatalf("failed to create doc: %v", err)
 34	}
 35	defer doc.Destroy()
 36
 37	// Get fragment and create waypoint
 38	frag, err := doc.GetXmlFragment("main")
 39	if err != nil {
 40		t.Fatalf("failed to get fragment: %v", err)
 41	}
 42	defer frag.Destroy()
 43
 44	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
 45		elem, err := frag.InsertElement(txn, 0, "waypoint")
 46		if err != nil {
 47			return err
 48		}
 49		defer elem.Destroy()
 50
 51		idInput, _ := ygo.JSON("wp1")
 52		if err := elem.SetAttribute(txn, "id", idInput); err != nil {
 53			return err
 54		}
 55
 56		// Set initial empty "from"
 57		fromInput, _ := ygo.JSON(map[string]Route{})
 58		return elem.SetAttribute(txn, "from", fromInput)
 59	})
 60	if err != nil {
 61		t.Fatalf("failed to setup waypoint: %v", err)
 62	}
 63
 64	// Apply route update
 65	updates := []RouteUpdate{
 66		{
 67			WaypointID: "wp1",
 68			RouteKey:   "wp0",
 69			Route: Route{
 70				ID:       "abc123",
 71				Distance: 100.5,
 72				Time:     60.2,
 73				Bbox:     []float64{-120.0, 38.0, -119.0, 39.0},
 74			},
 75		},
 76	}
 77
 78	err = UpdateWaypointRoutes(doc, updates)
 79	if err != nil {
 80		t.Fatalf("failed to update routes: %v", err)
 81	}
 82
 83	// Verify update was applied
 84	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
 85		childIter, err := frag.ChildIter(txn)
 86		if err != nil {
 87			return err
 88		}
 89
 90		for {
 91			child, err := childIter.Next()
 92			if err != nil {
 93				return err
 94			}
 95			if child == nil {
 96				break
 97			}
 98
 99			if child.Tag() != ygo.TagXmlElem {
100				child.Destroy()
101				continue
102			}
103
104			xmlElem, err := child.ToXmlElement()
105			child.Destroy()
106			if err != nil {
107				return err
108			}
109			defer xmlElem.Destroy()
110
111			// Get from attribute
112			attrIter, err := xmlElem.AttrIter(txn)
113			if err != nil {
114				return err
115			}
116
117			for {
118				attr, err := attrIter.Next()
119				if err != nil {
120					attrIter.Destroy()
121					return err
122				}
123				if attr == nil {
124					break
125				}
126
127				if attr.Name == "from" {
128					// Read raw JSON bytes and unmarshal directly
129					if jsonBytes, ok := attr.Value.JSONBytes(); ok {
130						var fromMap map[string]Route
131						if err := json.Unmarshal(jsonBytes, &fromMap); err != nil {
132							attr.Destroy()
133							attrIter.Destroy()
134							return err
135						}
136						if routeData, ok := fromMap["wp0"]; ok {
137							if routeData.ID != "abc123" {
138								t.Errorf("expected route ID abc123, got %v", routeData.ID)
139							}
140							if routeData.Distance != 100.5 {
141								t.Errorf("expected distance 100.5, got %v", routeData.Distance)
142							}
143						} else {
144							t.Error("route data not found in from map")
145						}
146					} else {
147						t.Error("from attribute is not valid JSON")
148					}
149				}
150				attr.Destroy()
151			}
152			attrIter.Destroy()
153		}
154
155		return nil
156	})
157	if err != nil {
158		t.Fatalf("failed to verify update: %v", err)
159	}
160}
161
162func TestRouteUpdate_Struct(t *testing.T) {
163	// Test the RouteUpdate struct
164	ru := RouteUpdate{
165		WaypointID: "wp2",
166		RouteKey:   "wp1:adv(80)",
167		Route: Route{
168			ID:       "def456",
169			Distance: 200.0,
170			Time:     120.0,
171			Bbox:     []float64{-121.0, 37.0, -120.0, 38.0},
172		},
173	}
174
175	if ru.WaypointID != "wp2" {
176		t.Error("WaypointID mismatch")
177	}
178	if ru.RouteKey != "wp1:adv(80)" {
179		t.Error("RouteKey mismatch")
180	}
181	if ru.Route.ID != "def456" {
182		t.Error("Route.ID mismatch")
183	}
184}
185
186func TestUpdateWaypointRoutes_WithPointUpdate(t *testing.T) {
187	doc, err := ygo.NewDoc()
188	if err != nil {
189		t.Fatalf("failed to create doc: %v", err)
190	}
191	defer doc.Destroy()
192
193	// Get fragment and create waypoint
194	frag, err := doc.GetXmlFragment("main")
195	if err != nil {
196		t.Fatalf("failed to get fragment: %v", err)
197	}
198	defer frag.Destroy()
199
200	// Set initial point
201	initialPoint := []float64{-120.0, 38.0}
202
203	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
204		elem, err := frag.InsertElement(txn, 0, "waypoint")
205		if err != nil {
206			return err
207		}
208		defer elem.Destroy()
209
210		idInput, _ := ygo.JSON("wp1")
211		if err := elem.SetAttribute(txn, "id", idInput); err != nil {
212			return err
213		}
214
215		pointInput, _ := ygo.JSON(initialPoint)
216		if err := elem.SetAttribute(txn, "point", pointInput); err != nil {
217			return err
218		}
219
220		// Set initial empty "from"
221		fromInput, _ := ygo.JSON(map[string]Route{})
222		return elem.SetAttribute(txn, "from", fromInput)
223	})
224	if err != nil {
225		t.Fatalf("failed to setup waypoint: %v", err)
226	}
227
228	// Apply route update with point snapping
229	newPoint := []float64{-119.5, 38.5}
230	updates := []RouteUpdate{
231		{
232			WaypointID: "wp1",
233			RouteKey:   "wp0",
234			Route: Route{
235				ID:       "abc123",
236				Distance: 100.5,
237				Time:     60.2,
238				Bbox:     []float64{-120.0, 38.0, -119.0, 39.0},
239			},
240			NewPoint: newPoint,
241		},
242	}
243
244	err = UpdateWaypointRoutes(doc, updates)
245	if err != nil {
246		t.Fatalf("failed to update routes: %v", err)
247	}
248
249	// Verify both route and point updates were applied
250	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
251		childIter, err := frag.ChildIter(txn)
252		if err != nil {
253			return err
254		}
255
256		for {
257			child, err := childIter.Next()
258			if err != nil {
259				return err
260			}
261			if child == nil {
262				break
263			}
264
265			if child.Tag() != ygo.TagXmlElem {
266				child.Destroy()
267				continue
268			}
269
270			xmlElem, err := child.ToXmlElement()
271			child.Destroy()
272			if err != nil {
273				return err
274			}
275			defer xmlElem.Destroy()
276
277			// Get attributes
278			attrIter, err := xmlElem.AttrIter(txn)
279			if err != nil {
280				return err
281			}
282
283			var foundFrom, foundPoint bool
284
285			for {
286				attr, err := attrIter.Next()
287				if err != nil {
288					attrIter.Destroy()
289					return err
290				}
291				if attr == nil {
292					break
293				}
294
295				if attr.Name == "from" {
296					foundFrom = true
297					// Read raw JSON bytes and unmarshal directly
298					if jsonBytes, ok := attr.Value.JSONBytes(); ok {
299						var fromMap map[string]Route
300						if err := json.Unmarshal(jsonBytes, &fromMap); err != nil {
301							attr.Destroy()
302							attrIter.Destroy()
303							return err
304						}
305						if routeData, ok := fromMap["wp0"]; ok {
306							if routeData.ID != "abc123" {
307								t.Errorf("expected route ID abc123, got %v", routeData.ID)
308							}
309						} else {
310							t.Error("route data not found in from map")
311						}
312					} else {
313						t.Error("from attribute is not valid JSON")
314					}
315			} else if attr.Name == "point" {
316				foundPoint = true
317				// Read raw JSON bytes and unmarshal directly
318				if jsonBytes, ok := attr.Value.JSONBytes(); ok {
319					var point []float64
320					if err := json.Unmarshal(jsonBytes, &point); err != nil {
321						attr.Destroy()
322						attrIter.Destroy()
323						return err
324					}
325					if len(point) != 2 {
326						t.Errorf("expected point array of length 2, got %d", len(point))
327					} else if point[0] != -119.5 || point[1] != 38.5 {
328						t.Errorf("expected point [-119.5, 38.5], got [%v, %v]", point[0], point[1])
329					}
330				} else {
331					t.Error("point attribute is not valid JSON")
332				}
333			}
334				attr.Destroy()
335			}
336			attrIter.Destroy()
337
338			if !foundFrom {
339				t.Error("from attribute not found")
340			}
341			if !foundPoint {
342				t.Error("point attribute not found")
343			}
344		}
345
346		return nil
347	})
348	if err != nil {
349		t.Fatalf("failed to verify update: %v", err)
350	}
351}
352
353func TestUpdateSourceWaypointPoints_EmptyUpdates(t *testing.T) {
354	doc, err := ygo.NewDoc()
355	if err != nil {
356		t.Fatalf("failed to create doc: %v", err)
357	}
358	defer doc.Destroy()
359
360	// Empty map - should return nil
361	err = UpdateSourceWaypointPoints(doc, map[string][]float64{})
362	if err != nil {
363		t.Fatalf("unexpected error: %v", err)
364	}
365
366	// Nil map - should return nil
367	err = UpdateSourceWaypointPoints(doc, nil)
368	if err != nil {
369		t.Fatalf("unexpected error: %v", err)
370	}
371}
372
373func TestUpdateSourceWaypointPoints_SingleWaypoint(t *testing.T) {
374	doc, err := ygo.NewDoc()
375	if err != nil {
376		t.Fatalf("failed to create doc: %v", err)
377	}
378	defer doc.Destroy()
379
380	// Get fragment and create waypoint
381	frag, err := doc.GetXmlFragment("main")
382	if err != nil {
383		t.Fatalf("failed to get fragment: %v", err)
384	}
385	defer frag.Destroy()
386
387	// Set initial point
388	initialPoint := []float64{-120.0, 38.0}
389
390	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
391		elem, err := frag.InsertElement(txn, 0, "waypoint")
392		if err != nil {
393			return err
394		}
395		defer elem.Destroy()
396
397		idInput, _ := ygo.JSON("wp1")
398		if err := elem.SetAttribute(txn, "id", idInput); err != nil {
399			return err
400		}
401
402		pointInput, _ := ygo.JSON(initialPoint)
403		return elem.SetAttribute(txn, "point", pointInput)
404	})
405	if err != nil {
406		t.Fatalf("failed to setup waypoint: %v", err)
407	}
408
409	// Apply point update
410	newPoint := []float64{-119.5, 38.5}
411	pointUpdates := map[string][]float64{
412		"wp1": newPoint,
413	}
414
415	err = UpdateSourceWaypointPoints(doc, pointUpdates)
416	if err != nil {
417		t.Fatalf("failed to update source waypoint points: %v", err)
418	}
419
420	// Verify update was applied
421	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
422		childIter, err := frag.ChildIter(txn)
423		if err != nil {
424			return err
425		}
426
427		for {
428			child, err := childIter.Next()
429			if err != nil {
430				return err
431			}
432			if child == nil {
433				break
434			}
435
436			if child.Tag() != ygo.TagXmlElem {
437				child.Destroy()
438				continue
439			}
440
441			xmlElem, err := child.ToXmlElement()
442			child.Destroy()
443			if err != nil {
444				return err
445			}
446			defer xmlElem.Destroy()
447
448			// Get point attribute
449			attrIter, err := xmlElem.AttrIter(txn)
450			if err != nil {
451				return err
452			}
453
454			for {
455				attr, err := attrIter.Next()
456				if err != nil {
457					attrIter.Destroy()
458					return err
459				}
460				if attr == nil {
461					break
462				}
463
464			if attr.Name == "point" {
465				// Read raw JSON bytes and unmarshal directly
466				if jsonBytes, ok := attr.Value.JSONBytes(); ok {
467					var point []float64
468					if err := json.Unmarshal(jsonBytes, &point); err != nil {
469						attr.Destroy()
470						attrIter.Destroy()
471						return err
472					}
473					if len(point) != 2 {
474						t.Errorf("expected point array of length 2, got %d", len(point))
475					} else if point[0] != -119.5 || point[1] != 38.5 {
476						t.Errorf("expected point [-119.5, 38.5], got [%v, %v]", point[0], point[1])
477					}
478				} else {
479					t.Error("point attribute is not valid JSON")
480				}
481			}
482				attr.Destroy()
483			}
484			attrIter.Destroy()
485		}
486
487		return nil
488	})
489	if err != nil {
490		t.Fatalf("failed to verify update: %v", err)
491	}
492}
493
494func TestUpdateSourceWaypointPoints_MultipleWaypoints(t *testing.T) {
495	doc, err := ygo.NewDoc()
496	if err != nil {
497		t.Fatalf("failed to create doc: %v", err)
498	}
499	defer doc.Destroy()
500
501	// Get fragment and create multiple waypoints
502	frag, err := doc.GetXmlFragment("main")
503	if err != nil {
504		t.Fatalf("failed to get fragment: %v", err)
505	}
506	defer frag.Destroy()
507
508	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
509		// Create waypoint 1
510		elem1, err := frag.InsertElement(txn, 0, "waypoint")
511		if err != nil {
512			return err
513		}
514		defer elem1.Destroy()
515
516		idInput1, _ := ygo.JSON("wp1")
517		if err := elem1.SetAttribute(txn, "id", idInput1); err != nil {
518			return err
519		}
520		pointInput1, _ := ygo.JSON([]float64{-120.0, 38.0})
521		if err := elem1.SetAttribute(txn, "point", pointInput1); err != nil {
522			return err
523		}
524
525		// Create waypoint 2
526		elem2, err := frag.InsertElement(txn, 1, "waypoint")
527		if err != nil {
528			return err
529		}
530		defer elem2.Destroy()
531
532		idInput2, _ := ygo.JSON("wp2")
533		if err := elem2.SetAttribute(txn, "id", idInput2); err != nil {
534			return err
535		}
536		pointInput2, _ := ygo.JSON([]float64{-121.0, 37.0})
537		if err := elem2.SetAttribute(txn, "point", pointInput2); err != nil {
538			return err
539		}
540
541		// Create waypoint 3 (not in update map)
542		elem3, err := frag.InsertElement(txn, 2, "waypoint")
543		if err != nil {
544			return err
545		}
546		defer elem3.Destroy()
547
548		idInput3, _ := ygo.JSON("wp3")
549		if err := elem3.SetAttribute(txn, "id", idInput3); err != nil {
550			return err
551		}
552		pointInput3, _ := ygo.JSON([]float64{-122.0, 36.0})
553		return elem3.SetAttribute(txn, "point", pointInput3)
554	})
555	if err != nil {
556		t.Fatalf("failed to setup waypoints: %v", err)
557	}
558
559	// Apply point updates for wp1 and wp2 only
560	pointUpdates := map[string][]float64{
561		"wp1": []float64{-119.5, 38.5},
562		"wp2": []float64{-120.5, 37.5},
563	}
564
565	err = UpdateSourceWaypointPoints(doc, pointUpdates)
566	if err != nil {
567		t.Fatalf("failed to update source waypoint points: %v", err)
568	}
569
570	// Verify updates
571	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
572		childIter, err := frag.ChildIter(txn)
573		if err != nil {
574			return err
575		}
576
577		points := make(map[string][]float64)
578
579		for {
580			child, err := childIter.Next()
581			if err != nil {
582				return err
583			}
584			if child == nil {
585				break
586			}
587
588			if child.Tag() != ygo.TagXmlElem {
589				child.Destroy()
590				continue
591			}
592
593			xmlElem, err := child.ToXmlElement()
594			child.Destroy()
595			if err != nil {
596				return err
597			}
598			defer xmlElem.Destroy()
599
600			// Get id and point attributes
601			attrIter, err := xmlElem.AttrIter(txn)
602			if err != nil {
603				return err
604			}
605
606			var id string
607			var point []float64
608
609			for {
610				attr, err := attrIter.Next()
611				if err != nil {
612					attrIter.Destroy()
613					return err
614				}
615				if attr == nil {
616					break
617				}
618
619			if attr.Name == "id" {
620				if str, ok := attr.Value.String(); ok {
621					id = str
622				}
623			} else if attr.Name == "point" {
624				// Read raw JSON bytes and unmarshal directly
625				if jsonBytes, ok := attr.Value.JSONBytes(); ok {
626					if err := json.Unmarshal(jsonBytes, &point); err != nil {
627						attr.Destroy()
628						attrIter.Destroy()
629						return err
630					}
631					if len(point) < 2 {
632						t.Errorf("expected point array of length >= 2, got %d", len(point))
633					}
634				} else {
635					t.Error("point attribute is not valid JSON")
636				}
637			}
638				attr.Destroy()
639			}
640			attrIter.Destroy()
641
642			if id != "" && point != nil {
643				points[id] = point
644			}
645		}
646
647		// Verify wp1 was updated
648		if p, ok := points["wp1"]; !ok || p[0] != -119.5 || p[1] != 38.5 {
649			t.Errorf("wp1: expected [-119.5, 38.5], got %v", p)
650		}
651
652		// Verify wp2 was updated
653		if p, ok := points["wp2"]; !ok || p[0] != -120.5 || p[1] != 37.5 {
654			t.Errorf("wp2: expected [-120.5, 37.5], got %v", p)
655		}
656
657		// Verify wp3 was NOT updated (should still have original value)
658		if p, ok := points["wp3"]; !ok || p[0] != -122.0 || p[1] != 36.0 {
659			t.Errorf("wp3: expected original [-122.0, 36.0], got %v", p)
660		}
661
662		return nil
663	})
664	if err != nil {
665		t.Fatalf("failed to verify updates: %v", err)
666	}
667}
668
669func TestUpdateSourceWaypointPoints_NestedWaypoint(t *testing.T) {
670	doc, err := ygo.NewDoc()
671	if err != nil {
672		t.Fatalf("failed to create doc: %v", err)
673	}
674	defer doc.Destroy()
675
676	// Get fragment and create nested structure
677	frag, err := doc.GetXmlFragment("main")
678	if err != nil {
679		t.Fatalf("failed to get fragment: %v", err)
680	}
681	defer frag.Destroy()
682
683	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
684		// Create a container element
685		container, err := frag.InsertElement(txn, 0, "day")
686		if err != nil {
687			return err
688		}
689		defer container.Destroy()
690
691		// Create waypoint inside container
692		elem, err := container.InsertElement(txn, 0, "waypoint")
693		if err != nil {
694			return err
695		}
696		defer elem.Destroy()
697
698		idInput, _ := ygo.JSON("wp-nested")
699		if err := elem.SetAttribute(txn, "id", idInput); err != nil {
700			return err
701		}
702		pointInput, _ := ygo.JSON([]float64{-120.0, 38.0})
703		return elem.SetAttribute(txn, "point", pointInput)
704	})
705	if err != nil {
706		t.Fatalf("failed to setup nested waypoint: %v", err)
707	}
708
709	// Apply point update
710	newPoint := []float64{-119.5, 38.5}
711	pointUpdates := map[string][]float64{
712		"wp-nested": newPoint,
713	}
714
715	err = UpdateSourceWaypointPoints(doc, pointUpdates)
716	if err != nil {
717		t.Fatalf("failed to update nested waypoint point: %v", err)
718	}
719
720	// Verify update was applied to nested waypoint
721	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
722		childIter, err := frag.ChildIter(txn)
723		if err != nil {
724			return err
725		}
726
727		for {
728			child, err := childIter.Next()
729			if err != nil {
730				return err
731			}
732			if child == nil {
733				break
734			}
735
736			if child.Tag() != ygo.TagXmlElem {
737				child.Destroy()
738				continue
739			}
740
741			xmlElem, err := child.ToXmlElement()
742			child.Destroy()
743			if err != nil {
744				return err
745			}
746			defer xmlElem.Destroy()
747
748			// Get container's children
749			containerIter, err := xmlElem.ChildIter(txn)
750			if err != nil {
751				return err
752			}
753
754			for {
755				containerChild, err := containerIter.Next()
756				if err != nil {
757					containerIter.Destroy()
758					return err
759				}
760				if containerChild == nil {
761					break
762				}
763
764				if containerChild.Tag() != ygo.TagXmlElem {
765					containerChild.Destroy()
766					continue
767				}
768
769				waypointElem, err := containerChild.ToXmlElement()
770				containerChild.Destroy()
771				if err != nil {
772					containerIter.Destroy()
773					return err
774				}
775				defer waypointElem.Destroy()
776
777				// Get point attribute
778				attrIter, err := waypointElem.AttrIter(txn)
779				if err != nil {
780					containerIter.Destroy()
781					return err
782				}
783
784				for {
785					attr, err := attrIter.Next()
786					if err != nil {
787						attrIter.Destroy()
788						containerIter.Destroy()
789						return err
790					}
791					if attr == nil {
792						break
793					}
794
795					if attr.Name == "point" {
796						// Read raw JSON bytes and unmarshal directly
797						if jsonBytes, ok := attr.Value.JSONBytes(); ok {
798							var point []float64
799							if err := json.Unmarshal(jsonBytes, &point); err != nil {
800								attr.Destroy()
801								attrIter.Destroy()
802								containerIter.Destroy()
803								return err
804							}
805							if len(point) != 2 {
806								t.Errorf("expected point array of length 2, got %d", len(point))
807							} else if point[0] != -119.5 || point[1] != 38.5 {
808								t.Errorf("expected point [-119.5, 38.5], got [%v, %v]", point[0], point[1])
809							}
810						} else {
811							t.Error("point attribute is not valid JSON")
812						}
813					}
814					attr.Destroy()
815				}
816				attrIter.Destroy()
817			}
818			containerIter.Destroy()
819		}
820
821		return nil
822	})
823	if err != nil {
824		t.Fatalf("failed to verify update: %v", err)
825	}
826}
827
828func TestUpdateWaypointRoutes_WithoutPointUpdate(t *testing.T) {
829	doc, err := ygo.NewDoc()
830	if err != nil {
831		t.Fatalf("failed to create doc: %v", err)
832	}
833	defer doc.Destroy()
834
835	// Get fragment and create waypoint
836	frag, err := doc.GetXmlFragment("main")
837	if err != nil {
838		t.Fatalf("failed to get fragment: %v", err)
839	}
840	defer frag.Destroy()
841
842	// Set initial point
843	initialPoint := []float64{-120.0, 38.0}
844
845	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
846		elem, err := frag.InsertElement(txn, 0, "waypoint")
847		if err != nil {
848			return err
849		}
850		defer elem.Destroy()
851
852		idInput, _ := ygo.JSON("wp1")
853		if err := elem.SetAttribute(txn, "id", idInput); err != nil {
854			return err
855		}
856
857		pointInput, _ := ygo.JSON(initialPoint)
858		if err := elem.SetAttribute(txn, "point", pointInput); err != nil {
859			return err
860		}
861
862		// Set initial empty "from"
863		fromInput, _ := ygo.JSON(map[string]Route{})
864		return elem.SetAttribute(txn, "from", fromInput)
865	})
866	if err != nil {
867		t.Fatalf("failed to setup waypoint: %v", err)
868	}
869
870	// Apply route update WITHOUT point snapping (NewPoint is nil)
871	updates := []RouteUpdate{
872		{
873			WaypointID: "wp1",
874			RouteKey:   "wp0",
875			Route: Route{
876				ID:       "abc123",
877				Distance: 100.5,
878				Time:     60.2,
879				Bbox:     []float64{-120.0, 38.0, -119.0, 39.0},
880			},
881			NewPoint: nil, // No point update
882		},
883	}
884
885	err = UpdateWaypointRoutes(doc, updates)
886	if err != nil {
887		t.Fatalf("failed to update routes: %v", err)
888	}
889
890	// Verify route was updated but point was NOT changed
891	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
892		childIter, err := frag.ChildIter(txn)
893		if err != nil {
894			return err
895		}
896
897		for {
898			child, err := childIter.Next()
899			if err != nil {
900				return err
901			}
902			if child == nil {
903				break
904			}
905
906			if child.Tag() != ygo.TagXmlElem {
907				child.Destroy()
908				continue
909			}
910
911			xmlElem, err := child.ToXmlElement()
912			child.Destroy()
913			if err != nil {
914				return err
915			}
916			defer xmlElem.Destroy()
917
918			// Get point attribute
919			attrIter, err := xmlElem.AttrIter(txn)
920			if err != nil {
921				return err
922			}
923
924			for {
925				attr, err := attrIter.Next()
926				if err != nil {
927					attrIter.Destroy()
928					return err
929				}
930				if attr == nil {
931					break
932				}
933
934			if attr.Name == "point" {
935				// Read raw JSON bytes and unmarshal directly
936				if jsonBytes, ok := attr.Value.JSONBytes(); ok {
937					var point []float64
938					if err := json.Unmarshal(jsonBytes, &point); err != nil {
939						attr.Destroy()
940						attrIter.Destroy()
941						return err
942					}
943					if len(point) != 2 {
944						t.Errorf("expected point array of length 2, got %d", len(point))
945					} else if point[0] != -120.0 || point[1] != 38.0 {
946						// Point should still be the initial value
947						t.Errorf("expected original point [-120.0, 38.0], got [%v, %v]", point[0], point[1])
948					}
949				} else {
950					t.Error("point attribute is not valid JSON")
951				}
952			}
953				attr.Destroy()
954			}
955			attrIter.Destroy()
956		}
957
958		return nil
959	})
960	if err != nil {
961		t.Fatalf("failed to verify update: %v", err)
962	}
963}