@@ -109,3 +109,133 @@ fn a_session_boundary_is_not_a_position() {
109109 Some ( Position :: Live )
110110 ) ;
111111}
112+
113+ // The same call, dispatched again by a resumed session that was stopped at the
114+ // argument prompt before it reached the callee.
115+ fn redispatched ( ) -> Vec < Record > {
116+ vec ! [
117+ record( 1 , "/task:" , State :: Begin ( Vec :: new( ) ) ) ,
118+ record( 2 , "/task:/1" , State :: Begin ( Vec :: new( ) ) ) ,
119+ record(
120+ 2 ,
121+ "/task:/1" ,
122+ State :: Invoke ( InvokeTarget :: Procedure ( "check:" . to_string( ) ) ) ,
123+ ) ,
124+ record( 0 , "/" , State :: Stop ) ,
125+ record( 0 , "/" , State :: Resume ) ,
126+ record(
127+ 2 ,
128+ "/task:/1" ,
129+ State :: Invoke ( InvokeTarget :: Procedure ( "check:" . to_string( ) ) ) ,
130+ ) ,
131+ record(
132+ 2 ,
133+ "/task:/1" ,
134+ State :: Invoke ( InvokeTarget :: Procedure ( "verify:" . to_string( ) ) ) ,
135+ ) ,
136+ ]
137+ }
138+
139+ #[ test]
140+ fn a_redispatched_call_is_one_place_to_stand ( ) {
141+ // Two sessions reaching the same call wrote the dispatch line twice, and
142+ // pressing Up walked both showing the same thing each time. The trail keeps
143+ // them; the cursor stops on the last. A second call in the same step writes
144+ // a different line at the same address, and stands on its own.
145+ let records = redispatched ( ) ;
146+ let trail = Trail :: new ( & records) ;
147+
148+ let at = trail
149+ . last ( )
150+ . expect ( "a position to open on" ) ;
151+ assert_eq ! ( at, Position :: At ( 6 ) ) ;
152+ assert_eq ! ( trail. step( at, Motion :: Up ) , Some ( Position :: At ( 5 ) ) ) ;
153+ assert_eq ! (
154+ trail. step( Position :: At ( 5 ) , Motion :: Up ) ,
155+ Some ( Position :: At ( 1 ) )
156+ ) ;
157+ }
158+
159+ #[ test]
160+ fn two_invocations_of_one_procedure_both_stand ( ) {
161+ // A procedure invoked twice records both at its own path, so the path is
162+ // not what tells two executions apart — the serial is. Collapsing by path
163+ // would have swallowed the first call's whole subtree.
164+ let records = vec ! [
165+ record( 1 , "/task:" , State :: Begin ( Vec :: new( ) ) ) ,
166+ record( 2 , "/task:/1" , State :: Begin ( Vec :: new( ) ) ) ,
167+ record( 3 , "/task:/check:" , State :: Begin ( Vec :: new( ) ) ) ,
168+ record( 3 , "/task:/check:" , State :: Done ( None ) ) ,
169+ record( 2 , "/task:/1" , State :: Done ( None ) ) ,
170+ record( 4 , "/task:/2" , State :: Begin ( Vec :: new( ) ) ) ,
171+ record( 5 , "/task:/check:" , State :: Begin ( Vec :: new( ) ) ) ,
172+ record( 5 , "/task:/check:" , State :: Done ( None ) ) ,
173+ record( 4 , "/task:/2" , State :: Done ( None ) ) ,
174+ ] ;
175+ let trail = Trail :: new ( & records) ;
176+
177+ let mut at = Position :: At ( 8 ) ;
178+ for expected in [ 7 , 6 , 5 , 4 , 3 , 2 , 1 , 0 ] {
179+ at = trail
180+ . step ( at, Motion :: Up )
181+ . expect ( "every record stands as its own position" ) ;
182+ assert_eq ! ( at, Position :: At ( expected) ) ;
183+ }
184+ }
185+
186+ #[ test]
187+ fn an_amended_answer_is_the_only_one_review_reaches ( ) {
188+ // Withdrawing an answer and giving a different one writes a second outcome
189+ // at the same path under a fresh serial. Navigating back shows the value
190+ // the position carries now; the one it replaced, the `Revoke` that took it
191+ // away, and the entry line above it are all recorded and none of them are
192+ // places to go.
193+ let records = vec ! [
194+ record( 1 , "/task:" , State :: Begin ( Vec :: new( ) ) ) ,
195+ record( 2 , "/task:/1" , State :: Begin ( Vec :: new( ) ) ) ,
196+ record( 2 , "/task:/1" , State :: Skip ) ,
197+ record( 2 , "/task:/1" , State :: Revoke ) ,
198+ record( 3 , "/task:/1" , State :: Begin ( Vec :: new( ) ) ) ,
199+ record( 3 , "/task:/1" , State :: Done ( None ) ) ,
200+ ] ;
201+ let trail = Trail :: new ( & records) ;
202+
203+ let at = trail
204+ . last ( )
205+ . expect ( "a position to open on" ) ;
206+ assert_eq ! ( at, Position :: At ( 5 ) ) ;
207+ assert_eq ! ( trail. step( at, Motion :: Up ) , Some ( Position :: At ( 4 ) ) ) ;
208+ assert_eq ! (
209+ trail. step( Position :: At ( 4 ) , Motion :: Up ) ,
210+ Some ( Position :: At ( 0 ) )
211+ ) ;
212+ }
213+
214+ #[ test]
215+ fn a_revoked_scope_takes_what_it_held_with_it ( ) {
216+ // Revoking a call withdraws the whole subtree beneath it: the replay redoes
217+ // that work under fresh serials, so the records the first pass left are no
218+ // more current than the call that held them.
219+ let records = vec ! [
220+ record( 1 , "/task:" , State :: Begin ( Vec :: new( ) ) ) ,
221+ record( 2 , "/task:/check:" , State :: Begin ( Vec :: new( ) ) ) ,
222+ record( 3 , "/task:/check:/1" , State :: Begin ( Vec :: new( ) ) ) ,
223+ record( 3 , "/task:/check:/1" , State :: Done ( None ) ) ,
224+ record( 2 , "/task:/check:" , State :: Done ( None ) ) ,
225+ record( 2 , "/task:/check:" , State :: Revoke ) ,
226+ record( 4 , "/task:/check:" , State :: Begin ( Vec :: new( ) ) ) ,
227+ record( 5 , "/task:/check:/1" , State :: Begin ( Vec :: new( ) ) ) ,
228+ record( 5 , "/task:/check:/1" , State :: Done ( None ) ) ,
229+ record( 4 , "/task:/check:" , State :: Done ( None ) ) ,
230+ ] ;
231+ let trail = Trail :: new ( & records) ;
232+
233+ let mut at = Position :: At ( 9 ) ;
234+ for expected in [ 8 , 7 , 6 , 0 ] {
235+ at = trail
236+ . step ( at, Motion :: Up )
237+ . expect ( "the standing execution walks back to the root" ) ;
238+ assert_eq ! ( at, Position :: At ( expected) ) ;
239+ }
240+ assert_eq ! ( trail. step( at, Motion :: Up ) , None ) ;
241+ }
0 commit comments