@@ -3,8 +3,10 @@ import {
33 partitionWorkspacesByAge ,
44 formatDaysThreshold ,
55 AGE_THRESHOLDS_DAYS ,
6+ buildSortedWorkspacesByProject ,
67} from "./workspaceFiltering" ;
78import type { FrontendWorkspaceMetadata } from "@/common/types/workspace" ;
9+ import type { ProjectConfig } from "@/common/types/project" ;
810import { DEFAULT_RUNTIME_CONFIG } from "@/common/constants/workspace" ;
911
1012describe ( "partitionWorkspacesByAge" , ( ) => {
@@ -173,3 +175,145 @@ describe("formatDaysThreshold", () => {
173175 expect ( formatDaysThreshold ( 30 ) ) . toBe ( "30 days" ) ;
174176 } ) ;
175177} ) ;
178+
179+ describe ( "buildSortedWorkspacesByProject" , ( ) => {
180+ const createWorkspace = (
181+ id : string ,
182+ projectPath : string ,
183+ status ?: "creating"
184+ ) : FrontendWorkspaceMetadata => ( {
185+ id,
186+ name : `workspace-${ id } ` ,
187+ projectName : projectPath . split ( "/" ) . pop ( ) ?? "unknown" ,
188+ projectPath,
189+ namedWorkspacePath : `${ projectPath } /workspace-${ id } ` ,
190+ runtimeConfig : DEFAULT_RUNTIME_CONFIG ,
191+ status,
192+ } ) ;
193+
194+ it ( "should include workspaces from persisted config" , ( ) => {
195+ const projects = new Map < string , ProjectConfig > ( [
196+ [ "/project/a" , { workspaces : [ { path : "/a/ws1" , id : "ws1" } ] } ] ,
197+ ] ) ;
198+ const metadata = new Map < string , FrontendWorkspaceMetadata > ( [
199+ [ "ws1" , createWorkspace ( "ws1" , "/project/a" ) ] ,
200+ ] ) ;
201+
202+ const result = buildSortedWorkspacesByProject ( projects , metadata , { } ) ;
203+
204+ expect ( result . get ( "/project/a" ) ) . toHaveLength ( 1 ) ;
205+ expect ( result . get ( "/project/a" ) ?. [ 0 ] . id ) . toBe ( "ws1" ) ;
206+ } ) ;
207+
208+ it ( "should include pending workspaces not yet in config" , ( ) => {
209+ const projects = new Map < string , ProjectConfig > ( [
210+ [ "/project/a" , { workspaces : [ { path : "/a/ws1" , id : "ws1" } ] } ] ,
211+ ] ) ;
212+ const metadata = new Map < string , FrontendWorkspaceMetadata > ( [
213+ [ "ws1" , createWorkspace ( "ws1" , "/project/a" ) ] ,
214+ [ "pending1" , createWorkspace ( "pending1" , "/project/a" , "creating" ) ] ,
215+ ] ) ;
216+
217+ const result = buildSortedWorkspacesByProject ( projects , metadata , { } ) ;
218+
219+ expect ( result . get ( "/project/a" ) ) . toHaveLength ( 2 ) ;
220+ expect ( result . get ( "/project/a" ) ?. map ( ( w ) => w . id ) ) . toContain ( "ws1" ) ;
221+ expect ( result . get ( "/project/a" ) ?. map ( ( w ) => w . id ) ) . toContain ( "pending1" ) ;
222+ } ) ;
223+
224+ it ( "should handle multiple concurrent pending workspaces" , ( ) => {
225+ const projects = new Map < string , ProjectConfig > ( [ [ "/project/a" , { workspaces : [ ] } ] ] ) ;
226+ const metadata = new Map < string , FrontendWorkspaceMetadata > ( [
227+ [ "pending1" , createWorkspace ( "pending1" , "/project/a" , "creating" ) ] ,
228+ [ "pending2" , createWorkspace ( "pending2" , "/project/a" , "creating" ) ] ,
229+ [ "pending3" , createWorkspace ( "pending3" , "/project/a" , "creating" ) ] ,
230+ ] ) ;
231+
232+ const result = buildSortedWorkspacesByProject ( projects , metadata , { } ) ;
233+
234+ expect ( result . get ( "/project/a" ) ) . toHaveLength ( 3 ) ;
235+ } ) ;
236+
237+ it ( "should add pending workspaces for projects not yet in config" , ( ) => {
238+ const projects = new Map < string , ProjectConfig > ( ) ;
239+ const metadata = new Map < string , FrontendWorkspaceMetadata > ( [
240+ [ "pending1" , createWorkspace ( "pending1" , "/new/project" , "creating" ) ] ,
241+ ] ) ;
242+
243+ const result = buildSortedWorkspacesByProject ( projects , metadata , { } ) ;
244+
245+ expect ( result . get ( "/new/project" ) ) . toHaveLength ( 1 ) ;
246+ expect ( result . get ( "/new/project" ) ?. [ 0 ] . id ) . toBe ( "pending1" ) ;
247+ } ) ;
248+
249+ it ( "should sort workspaces by recency (most recent first)" , ( ) => {
250+ const now = Date . now ( ) ;
251+ const projects = new Map < string , ProjectConfig > ( [
252+ [
253+ "/project/a" ,
254+ {
255+ workspaces : [
256+ { path : "/a/ws1" , id : "ws1" } ,
257+ { path : "/a/ws2" , id : "ws2" } ,
258+ { path : "/a/ws3" , id : "ws3" } ,
259+ ] ,
260+ } ,
261+ ] ,
262+ ] ) ;
263+ const metadata = new Map < string , FrontendWorkspaceMetadata > ( [
264+ [ "ws1" , createWorkspace ( "ws1" , "/project/a" ) ] ,
265+ [ "ws2" , createWorkspace ( "ws2" , "/project/a" ) ] ,
266+ [ "ws3" , createWorkspace ( "ws3" , "/project/a" ) ] ,
267+ ] ) ;
268+ const recency = {
269+ ws1 : now - 3000 , // oldest
270+ ws2 : now - 1000 , // newest
271+ ws3 : now - 2000 , // middle
272+ } ;
273+
274+ const result = buildSortedWorkspacesByProject ( projects , metadata , recency ) ;
275+
276+ expect ( result . get ( "/project/a" ) ?. map ( ( w ) => w . id ) ) . toEqual ( [ "ws2" , "ws3" , "ws1" ] ) ;
277+ } ) ;
278+
279+ it ( "should not duplicate workspaces that exist in both config and have creating status" , ( ) => {
280+ // Edge case: workspace was saved to config but still has status: "creating"
281+ // (this shouldn't happen in practice but tests defensive coding)
282+ const projects = new Map < string , ProjectConfig > ( [
283+ [ "/project/a" , { workspaces : [ { path : "/a/ws1" , id : "ws1" } ] } ] ,
284+ ] ) ;
285+ const metadata = new Map < string , FrontendWorkspaceMetadata > ( [
286+ [ "ws1" , createWorkspace ( "ws1" , "/project/a" , "creating" ) ] ,
287+ ] ) ;
288+
289+ const result = buildSortedWorkspacesByProject ( projects , metadata , { } ) ;
290+
291+ expect ( result . get ( "/project/a" ) ) . toHaveLength ( 1 ) ;
292+ expect ( result . get ( "/project/a" ) ?. [ 0 ] . id ) . toBe ( "ws1" ) ;
293+ } ) ;
294+
295+ it ( "should skip workspaces with no id in config" , ( ) => {
296+ const projects = new Map < string , ProjectConfig > ( [
297+ [ "/project/a" , { workspaces : [ { path : "/a/legacy" } , { path : "/a/ws1" , id : "ws1" } ] } ] ,
298+ ] ) ;
299+ const metadata = new Map < string , FrontendWorkspaceMetadata > ( [
300+ [ "ws1" , createWorkspace ( "ws1" , "/project/a" ) ] ,
301+ ] ) ;
302+
303+ const result = buildSortedWorkspacesByProject ( projects , metadata , { } ) ;
304+
305+ expect ( result . get ( "/project/a" ) ) . toHaveLength ( 1 ) ;
306+ expect ( result . get ( "/project/a" ) ?. [ 0 ] . id ) . toBe ( "ws1" ) ;
307+ } ) ;
308+
309+ it ( "should skip config workspaces with no matching metadata" , ( ) => {
310+ const projects = new Map < string , ProjectConfig > ( [
311+ [ "/project/a" , { workspaces : [ { path : "/a/ws1" , id : "ws1" } ] } ] ,
312+ ] ) ;
313+ const metadata = new Map < string , FrontendWorkspaceMetadata > ( ) ; // empty
314+
315+ const result = buildSortedWorkspacesByProject ( projects , metadata , { } ) ;
316+
317+ expect ( result . get ( "/project/a" ) ) . toHaveLength ( 0 ) ;
318+ } ) ;
319+ } ) ;
0 commit comments