@@ -13,6 +13,7 @@ import { AsyncResult } from "effect/reactivity";
1313import { afterEach , beforeEach , describe , expect , it , vi } from "vite-plus/test" ;
1414
1515const state = vi . hoisted ( ( ) => ( {
16+ connectedEnvironmentIds : [ ] as EnvironmentId [ ] ,
1617 pendingOrder : null as object | null ,
1718 dropBusy : false ,
1819 failedAction : null as string | null ,
@@ -60,6 +61,9 @@ vi.mock("../../state/session", () => ({
6061 readEnvironmentScope : ( environmentId : string , scope : string ) =>
6162 state . scopes . get ( environmentId ) ?. has ( scope ) === true ,
6263} ) ) ;
64+ vi . mock ( "../../state/presentation" , ( ) => ( {
65+ connectedEnvironmentIdsAtom : "connected-environments" ,
66+ } ) ) ;
6367vi . mock ( "../../state/server" , ( ) => ( {
6468 environmentServerConfigsAtom : "server-configs" ,
6569} ) ) ;
@@ -69,28 +73,30 @@ vi.mock("../../state/atom-registry", () => ({
6973 state . dropBusy = value ;
7074 } ,
7175 get : ( atom : string ) =>
72- atom === "thread-drop-busy"
73- ? state . dropBusy
74- : atom === "thread-shells"
75- ? state . shells
76- : atom === "queued-thread-keys"
77- ? new Set < string > ( )
78- : new Map (
79- [ ...state . scopes . keys ( ) ] . map ( ( environmentId ) => [
80- environmentId ,
81- {
82- environment : {
83- capabilities : {
84- threadSettlement : true ,
85- threadSnooze : true ,
86- threadPinning : true ,
87- threadPinReorder : true ,
88- threadTitleRegeneration : true ,
76+ atom === "connected-environments"
77+ ? state . connectedEnvironmentIds
78+ : atom === "thread-drop-busy"
79+ ? state . dropBusy
80+ : atom === "thread-shells"
81+ ? state . shells
82+ : atom === "queued-thread-keys"
83+ ? new Set < string > ( )
84+ : new Map (
85+ [ ...state . scopes . keys ( ) ] . map ( ( environmentId ) => [
86+ environmentId ,
87+ {
88+ environment : {
89+ capabilities : {
90+ threadSettlement : true ,
91+ threadSnooze : true ,
92+ threadPinning : true ,
93+ threadPinReorder : true ,
94+ threadTitleRegeneration : true ,
95+ } ,
8996 } ,
9097 } ,
91- } ,
92- ] ) ,
93- ) ,
98+ ] ) ,
99+ ) ,
94100 } ,
95101} ) ) ;
96102vi . mock ( "../../state/use-atom-command" , ( ) => ( {
@@ -130,6 +136,7 @@ vi.mock("../../state/threads", () => ({
130136 "pin" ,
131137 "unpin" ,
132138 "reorderPin" ,
139+ "reorderActive" ,
133140 "updateMetadata" ,
134141 ] . map ( ( action ) => [
135142 action ,
@@ -179,6 +186,7 @@ const mutationCases = [
179186] as const ;
180187
181188beforeEach ( ( ) => {
189+ state . connectedEnvironmentIds = [ primaryEnvironmentId , otherEnvironmentId ] ;
182190 state . failedAction = null ;
183191 state . archivedRefreshes = [ ] ;
184192 state . pendingOrder = null ;
@@ -286,6 +294,76 @@ describe("pinned thread operation permissions", () => {
286294 } ) ;
287295} ) ;
288296
297+ describe ( "thread reorder connection availability" , ( ) => {
298+ it . each ( [ "active" , "pinned" ] as const ) (
299+ "rejects %s materialization involving a cached capable offline neighbor" ,
300+ async ( section ) => {
301+ const moved = makeThread ( {
302+ pinnedAt : section === "pinned" ? "2026-09-02T00:00:00.000Z" : null ,
303+ } ) ;
304+ state . shells = [
305+ moved ,
306+ makeThread ( {
307+ id : ThreadId . make ( "offline-neighbor" ) ,
308+ environmentId : otherEnvironmentId ,
309+ createdAt : "2026-09-02T00:00:00.000Z" ,
310+ pinnedAt : moved . pinnedAt ,
311+ } ) ,
312+ ] ;
313+ state . scopes . get ( otherEnvironmentId ) ?. add ( AuthOrchestrationOperateScope ) ;
314+ state . connectedEnvironmentIds = [ primaryEnvironmentId ] ;
315+
316+ const { moveThread } = useThreadListActions ( ) ;
317+ expect ( await moveThread ( moved , "up" ) ) . toBe ( false ) ;
318+ expect ( state . requests ) . toEqual ( [ ] ) ;
319+
320+ state . connectedEnvironmentIds = [ primaryEnvironmentId , otherEnvironmentId ] ;
321+
322+ expect ( await moveThread ( moved , "up" ) ) . toBe ( true ) ;
323+ expect ( state . requests ) . toHaveLength ( 2 ) ;
324+ expect ( new Set ( state . requests . map ( ( { environmentId } ) => environmentId ) ) ) . toEqual (
325+ new Set ( [ primaryEnvironmentId , otherEnvironmentId ] ) ,
326+ ) ;
327+ } ,
328+ ) ;
329+
330+ it . each ( [ "active" , "pinned" ] as const ) (
331+ "uses the latest connection state in a retained %s move callback" ,
332+ async ( section ) => {
333+ const moved = makeThread ( {
334+ pinnedAt : section === "pinned" ? "2026-09-02T00:00:00.000Z" : null ,
335+ pinOrderKey : section === "pinned" ? "m" : null ,
336+ activeOrderKey : section === "active" ? "m" : null ,
337+ } ) ;
338+ state . shells = [
339+ moved ,
340+ makeThread ( {
341+ id : ThreadId . make ( "offline-neighbor" ) ,
342+ environmentId : otherEnvironmentId ,
343+ pinnedAt : moved . pinnedAt ,
344+ pinOrderKey : section === "pinned" ? "g" : null ,
345+ activeOrderKey : section === "active" ? "g" : null ,
346+ } ) ,
347+ ] ;
348+ const { moveThread } = useThreadListActions ( ) ;
349+ state . connectedEnvironmentIds = [ ] ;
350+
351+ expect ( await moveThread ( moved , "up" ) ) . toBe ( false ) ;
352+ expect ( state . requests ) . toEqual ( [ ] ) ;
353+
354+ state . connectedEnvironmentIds = [ primaryEnvironmentId ] ;
355+
356+ expect ( await moveThread ( moved , "up" ) ) . toBe ( true ) ;
357+ expect ( state . requests ) . toEqual ( [
358+ expect . objectContaining ( {
359+ action : section === "pinned" ? "reorderPin" : "reorderActive" ,
360+ environmentId : primaryEnvironmentId ,
361+ } ) ,
362+ ] ) ;
363+ } ,
364+ ) ;
365+ } ) ;
366+
289367describe ( "thread action undo" , ( ) => {
290368 it . each ( [
291369 [ "settleThread" , "settle" , "Settled" , "unsettle" ] ,
0 commit comments