@@ -36,11 +36,37 @@ func TestSortByUpdatedDesc_Empty(t *testing.T) {
3636 sortByUpdatedDesc ([]store.BeadSummary {{ID : "bd-only" }})
3737}
3838
39+ func TestSortByDepthThenOldest (t * testing.T ) {
40+ now := time .Now ().UTC ()
41+ beads := []store.BeadSummary {
42+ {ID : "bd-d1-new" , BlockDepth : 1 , CreatedAt : now },
43+ {ID : "bd-d0-old" , BlockDepth : 0 , CreatedAt : now .Add (- 3 * time .Hour )},
44+ {ID : "bd-d0-new" , BlockDepth : 0 , CreatedAt : now },
45+ {ID : "bd-d2" , BlockDepth : 2 , CreatedAt : now },
46+ {ID : "bd-d1-old" , BlockDepth : 1 , CreatedAt : now .Add (- 1 * time .Hour )},
47+ }
48+
49+ sortByDepthThenOldest (beads )
50+
51+ expected := []string {"bd-d0-old" , "bd-d0-new" , "bd-d1-old" , "bd-d1-new" , "bd-d2" }
52+ for i , want := range expected {
53+ if beads [i ].ID != want {
54+ t .Errorf ("position %d: got %s, want %s" , i , beads [i ].ID , want )
55+ }
56+ }
57+ }
58+
59+ func TestSortByDepthThenOldest_Empty (t * testing.T ) {
60+ // Should not panic on empty or single-element slices.
61+ sortByDepthThenOldest (nil )
62+ sortByDepthThenOldest ([]store.BeadSummary {})
63+ sortByDepthThenOldest ([]store.BeadSummary {{ID : "bd-only" }})
64+ }
65+
3966func TestDashboardSortOrder (t * testing.T ) {
4067 srv := crudServer (t )
4168
42- // Create beads with different statuses.
43- // Create them in order so UpdatedAt differs.
69+ // Create two open beads with different creation times.
4470 b1 := createViaAPI (t , srv , map [string ]any {"title" : "Open old" , "status" : "open" })
4571 time .Sleep (10 * time .Millisecond )
4672 b2 := createViaAPI (t , srv , map [string ]any {"title" : "Open new" , "status" : "open" })
@@ -55,15 +81,16 @@ func TestDashboardSortOrder(t *testing.T) {
5581
5682 body := w .Body .String ()
5783
58- // "Open new" should appear before "Open old" in the HTML
84+ // Open section sorts by depth (asc) then oldest-first.
85+ // Both beads are unblocked (depth 0), so "Open old" should appear before "Open new".
5986 posNew := strings .Index (body , b2 .Title )
6087 posOld := strings .Index (body , b1 .Title )
6188
6289 if posNew == - 1 || posOld == - 1 {
6390 t .Fatalf ("expected both beads in HTML; got body:\n %s" , body )
6491 }
65- if posNew >= posOld {
66- t .Errorf ("expected %q (newer ) before %q (older ) in dashboard" , b2 .Title , b1 .Title )
92+ if posOld >= posNew {
93+ t .Errorf ("expected %q (older ) before %q (newer ) in dashboard Open section " , b1 .Title , b2 .Title )
6794 }
6895}
6996
@@ -423,8 +450,9 @@ func TestDashboardSortOrderNotReady(t *testing.T) {
423450 if posNew == - 1 || posOld == - 1 {
424451 t .Fatalf ("expected both not_ready beads in HTML" )
425452 }
426- if posNew >= posOld {
427- t .Errorf ("expected %q (newer) before %q (older) in Not Ready section" , b2 .Title , b1 .Title )
453+ // Not Ready section sorts by depth (asc) then oldest-first.
454+ if posOld >= posNew {
455+ t .Errorf ("expected %q (older) before %q (newer) in Not Ready section" , b1 .Title , b2 .Title )
428456 }
429457}
430458
@@ -460,11 +488,10 @@ func TestDashboardBlockedIndicator_OpenSectionBlocked(t *testing.T) {
460488
461489 body := getDashboard (t , srv )
462490
463- // The lock emoji must appear in the Open section row for the blocked bead.
464- // We verify by checking that the row containing the blocked bead ID also contains the lock emoji.
465- blockedRow := `<td>🔒</td><td><a href="/bead/default/` + blocked .ID + `">`
491+ // The lock emoji with depth must appear in the Open section row for the blocked bead.
492+ blockedRow := `<td>🔒1</td><td><a href="/bead/default/` + blocked .ID + `">`
466493 if ! strings .Contains (body , blockedRow ) {
467- t .Errorf ("expected lock emoji in Open section row for blocked bead, body:\n %s" , body )
494+ t .Errorf ("expected lock emoji with depth in Open section row for blocked bead, body:\n %s" , body )
468495 }
469496}
470497
@@ -489,9 +516,9 @@ func TestDashboardBlockedIndicator_NotReadySectionBlocked(t *testing.T) {
489516
490517 body := getDashboard (t , srv )
491518
492- blockedRow := `<td>🔒</td><td><a href="/bead/default/` + blocked .ID + `">`
519+ blockedRow := `<td>🔒1 </td><td><a href="/bead/default/` + blocked .ID + `">`
493520 if ! strings .Contains (body , blockedRow ) {
494- t .Errorf ("expected lock emoji in Not Ready section row for blocked bead, body:\n %s" , body )
521+ t .Errorf ("expected lock emoji with depth in Not Ready section row for blocked bead, body:\n %s" , body )
495522 }
496523}
497524
@@ -939,6 +966,58 @@ func TestBeadDetailNoCookieIntegration(t *testing.T) {
939966 }
940967}
941968
969+ func TestDashboardDepthShownNextToLock (t * testing.T ) {
970+ srv := crudServer (t )
971+
972+ // Create a chain: C blocks B blocks A → A has depth 2, B has depth 1
973+ c := createViaAPI (t , srv , map [string ]any {"title" : "Chain C" , "status" : "open" })
974+ b := createViaAPI (t , srv , map [string ]any {"title" : "Chain B" , "status" : "open" })
975+ a := createViaAPI (t , srv , map [string ]any {"title" : "Chain A" , "status" : "open" })
976+ linkBeads (t , srv , b .ID , c .ID )
977+ linkBeads (t , srv , a .ID , b .ID )
978+
979+ body := getDashboard (t , srv )
980+
981+ depth1Row := `<td>🔒1</td><td><a href="/bead/default/` + b .ID + `">`
982+ if ! strings .Contains (body , depth1Row ) {
983+ t .Errorf ("expected 🔒1 for depth-1 bead %s" , b .ID )
984+ }
985+
986+ depth2Row := `<td>🔒2</td><td><a href="/bead/default/` + a .ID + `">`
987+ if ! strings .Contains (body , depth2Row ) {
988+ t .Errorf ("expected 🔒2 for depth-2 bead %s" , a .ID )
989+ }
990+ }
991+
992+ func TestDashboardSortByDepthThenOldest (t * testing.T ) {
993+ srv := crudServer (t )
994+
995+ // Create: blocker (unblocked, depth 0), blocked (depth 1), another unblocked (depth 0, newer)
996+ blocker := createViaAPI (t , srv , map [string ]any {"title" : "Depth0 oldest" , "status" : "open" })
997+ time .Sleep (10 * time .Millisecond )
998+ blocked := createViaAPI (t , srv , map [string ]any {"title" : "Depth1 item" , "status" : "open" })
999+ linkBeads (t , srv , blocked .ID , blocker .ID )
1000+ time .Sleep (10 * time .Millisecond )
1001+ unblocked2 := createViaAPI (t , srv , map [string ]any {"title" : "Depth0 newest" , "status" : "open" })
1002+
1003+ body := getDashboard (t , srv )
1004+
1005+ // Expected order in Open section: Depth0 oldest, Depth0 newest, Depth1 item
1006+ pos0old := strings .Index (body , blocker .Title )
1007+ pos0new := strings .Index (body , unblocked2 .Title )
1008+ pos1 := strings .Index (body , blocked .Title )
1009+
1010+ if pos0old == - 1 || pos0new == - 1 || pos1 == - 1 {
1011+ t .Fatalf ("expected all three beads in HTML" )
1012+ }
1013+ if pos0old >= pos0new {
1014+ t .Errorf ("expected %q (depth 0, older) before %q (depth 0, newer)" , blocker .Title , unblocked2 .Title )
1015+ }
1016+ if pos0new >= pos1 {
1017+ t .Errorf ("expected %q (depth 0) before %q (depth 1)" , unblocked2 .Title , blocked .Title )
1018+ }
1019+ }
1020+
9421021func TestDashboardToggleButtonIconDark (t * testing.T ) {
9431022 srv := crudServer (t )
9441023 req := httptest .NewRequest (http .MethodGet , "/" , nil )
0 commit comments