@@ -3,7 +3,7 @@ import userEvent from "@testing-library/user-event";
33import { describe , expect , it , beforeEach } from "vitest" ;
44import { QueryClient , QueryClientProvider } from "@tanstack/react-query" ;
55import { TransportProvider } from "@connectrpc/connect-query" ;
6- import { createRouterTransport } from "@connectrpc/connect" ;
6+ import { Code , ConnectError , createRouterTransport } from "@connectrpc/connect" ;
77import { MemoryRouter } from "react-router" ;
88import { create } from "@bufbuild/protobuf" ;
99import { timestampFromDate } from "@bufbuild/protobuf/wkt" ;
@@ -18,6 +18,7 @@ import {
1818 ReviewAssistService ,
1919 ReviewCommentService ,
2020 RepositoryService ,
21+ WorkspaceSchema ,
2122 UnitTaskSchema ,
2223 SubTaskSchema ,
2324 RepositorySchema ,
@@ -34,6 +35,7 @@ import {
3435 NotificationType ,
3536 PrStatus ,
3637 AgentCliType ,
38+ WorkspaceType ,
3739 PullRequestRecordSchema ,
3840} from "./gen/v1/dexdex_pb" ;
3941
@@ -50,6 +52,9 @@ const localStorageMock = (() => {
5052
5153Object . defineProperty ( window , "localStorage" , { value : localStorageMock } ) ;
5254
55+ const DEFAULT_WORKSPACE_ID = "ws-default" ;
56+ const LEGACY_DEFAULT_WORKSPACE_ID = "workspace-default" ;
57+
5358// Mock proto data matching the old MOCK_TASKS shape
5459const mockUnitTasks = [
5560 create ( UnitTaskSchema , {
@@ -206,14 +211,14 @@ const mockPullRequests = [
206211const mockRepositories = [
207212 create ( RepositorySchema , {
208213 repositoryId : "repo-oss" ,
209- workspaceId : "workspace-default" ,
214+ workspaceId : DEFAULT_WORKSPACE_ID ,
210215 repositoryUrl : "https://github.com/example/oss" ,
211216 createdAt : timestampFromDate ( new Date ( "2026-03-10T00:00:00Z" ) ) ,
212217 updatedAt : timestampFromDate ( new Date ( "2026-03-10T00:00:00Z" ) ) ,
213218 } ) ,
214219 create ( RepositorySchema , {
215220 repositoryId : "repo-infra" ,
216- workspaceId : "workspace-default" ,
221+ workspaceId : DEFAULT_WORKSPACE_ID ,
217222 repositoryUrl : "https://github.com/example/infra" ,
218223 createdAt : timestampFromDate ( new Date ( "2026-03-10T00:00:00Z" ) ) ,
219224 updatedAt : timestampFromDate ( new Date ( "2026-03-10T00:00:00Z" ) ) ,
@@ -223,7 +228,7 @@ const mockRepositories = [
223228const mockRepositoryGroups = [
224229 create ( RepositoryGroupSchema , {
225230 repositoryGroupId : "repo-group-main" ,
226- workspaceId : "workspace-default" ,
231+ workspaceId : DEFAULT_WORKSPACE_ID ,
227232 members : [
228233 create ( RepositoryGroupMemberSchema , {
229234 repositoryId : "repo-oss" ,
@@ -238,11 +243,35 @@ const mockRepositoryGroups = [
238243] ;
239244
240245const mockWorkspaceSettings = create ( WorkspaceSettingsSchema , {
241- workspaceId : "workspace-default" ,
246+ workspaceId : DEFAULT_WORKSPACE_ID ,
242247 defaultAgentCliType : AgentCliType . CLAUDE_CODE ,
243248} ) ;
244249
245- function createTestTransport ( ) {
250+ interface TestTransportOptions {
251+ workspaces ?: Array < {
252+ workspaceId : string ;
253+ name : string ;
254+ type ?: WorkspaceType ;
255+ } > ;
256+ createRepositoryErrorMessage ?: string ;
257+ }
258+
259+ function createTestTransport ( options : TestTransportOptions = { } ) {
260+ const workspaces = ( options . workspaces ?? [
261+ {
262+ workspaceId : DEFAULT_WORKSPACE_ID ,
263+ name : "Default Workspace" ,
264+ type : WorkspaceType . LOCAL_ENDPOINT ,
265+ } ,
266+ ] ) . map ( ( workspace ) =>
267+ create ( WorkspaceSchema , {
268+ workspaceId : workspace . workspaceId ,
269+ name : workspace . name ,
270+ type : workspace . type ?? WorkspaceType . LOCAL_ENDPOINT ,
271+ createdAt : timestampFromDate ( new Date ( "2026-03-10T00:00:00Z" ) ) ,
272+ } ) ,
273+ ) ;
274+
246275 return createRouterTransport ( ( router ) => {
247276 router . service ( TaskService , {
248277 listUnitTasks : ( ) => ( { unitTasks : mockUnitTasks } ) ,
@@ -290,8 +319,27 @@ function createTestTransport() {
290319 stopAgentSession : ( ) => ( { } ) ,
291320 } ) ;
292321 router . service ( WorkspaceService , {
293- getWorkspace : ( ) => ( { workspace : undefined } ) ,
294- listWorkspaces : ( ) => ( { workspaces : [ ] } ) ,
322+ getWorkspace : ( req ) => ( {
323+ workspace : workspaces . find ( ( workspace ) => workspace . workspaceId === req . workspaceId ) ,
324+ } ) ,
325+ listWorkspaces : ( ) => ( { workspaces } ) ,
326+ createWorkspace : ( req ) => {
327+ const createdWorkspace = create ( WorkspaceSchema , {
328+ workspaceId : `ws-${ Date . now ( ) } ` ,
329+ name : req . name ,
330+ type : req . type ,
331+ createdAt : timestampFromDate ( new Date ( ) ) ,
332+ } ) ;
333+ workspaces . push ( createdWorkspace ) ;
334+ return { workspace : createdWorkspace } ;
335+ } ,
336+ setActiveWorkspace : ( req ) => {
337+ const workspace = workspaces . find ( ( item ) => item . workspaceId === req . workspaceId ) ;
338+ if ( ! workspace ) {
339+ throw new Error ( `workspace not found: ${ req . workspaceId } ` ) ;
340+ }
341+ return { workspace } ;
342+ } ,
295343 getWorkspaceWorkStatus : ( ) => ( { status : 0 } ) ,
296344 getWorkspaceSettings : ( ) => ( { settings : mockWorkspaceSettings } ) ,
297345 updateWorkspaceSettings : ( req ) => ( {
@@ -315,15 +363,20 @@ function createTestTransport() {
315363 router . service ( RepositoryService , {
316364 getRepository : ( ) => ( { repository : mockRepositories [ 0 ] } ) ,
317365 listRepositories : ( ) => ( { repositories : mockRepositories } ) ,
318- createRepository : ( req ) => ( {
319- repository : create ( RepositorySchema , {
320- repositoryId : `repo-${ Date . now ( ) } ` ,
321- workspaceId : req . workspaceId ,
322- repositoryUrl : req . repositoryUrl ,
323- createdAt : timestampFromDate ( new Date ( ) ) ,
324- updatedAt : timestampFromDate ( new Date ( ) ) ,
325- } ) ,
326- } ) ,
366+ createRepository : async ( req ) => {
367+ if ( options . createRepositoryErrorMessage ) {
368+ throw new ConnectError ( options . createRepositoryErrorMessage , Code . Internal ) ;
369+ }
370+ return {
371+ repository : create ( RepositorySchema , {
372+ repositoryId : `repo-${ Date . now ( ) } ` ,
373+ workspaceId : req . workspaceId ,
374+ repositoryUrl : req . repositoryUrl ,
375+ createdAt : timestampFromDate ( new Date ( ) ) ,
376+ updatedAt : timestampFromDate ( new Date ( ) ) ,
377+ } ) ,
378+ } ;
379+ } ,
327380 updateRepository : ( req ) => ( {
328381 repository : create ( RepositorySchema , {
329382 repositoryId : req . repositoryId ,
@@ -379,7 +432,13 @@ function createTestTransport() {
379432 } ) ;
380433}
381434
382- function renderWithProviders ( ui : React . ReactElement , { initialEntries = [ "/tasks" ] } : { initialEntries ?: string [ ] } = { } ) {
435+ function renderWithProviders (
436+ ui : React . ReactElement ,
437+ {
438+ initialEntries = [ "/tasks" ] ,
439+ transportOptions,
440+ } : { initialEntries ?: string [ ] ; transportOptions ?: TestTransportOptions } = { } ,
441+ ) {
383442 const queryClient = new QueryClient ( {
384443 defaultOptions : {
385444 queries : {
@@ -390,7 +449,7 @@ function renderWithProviders(ui: React.ReactElement, { initialEntries = ["/tasks
390449 } ) ;
391450 return render (
392451 < QueryClientProvider client = { queryClient } >
393- < TransportProvider transport = { createTestTransport ( ) } >
452+ < TransportProvider transport = { createTestTransport ( transportOptions ) } >
394453 < MemoryRouter initialEntries = { initialEntries } >
395454 { ui }
396455 </ MemoryRouter >
@@ -402,6 +461,7 @@ function renderWithProviders(ui: React.ReactElement, { initialEntries = ["/tasks
402461beforeEach ( ( ) => {
403462 localStorageMock . clear ( ) ;
404463 document . documentElement . classList . remove ( "dark" ) ;
464+ localStorageMock . setItem ( "dexdex-active-workspace-id" , DEFAULT_WORKSPACE_ID ) ;
405465} ) ;
406466
407467describe ( "App" , ( ) => {
@@ -755,4 +815,75 @@ describe("App", () => {
755815 expect ( await screen . findByText ( "Plan approval needed" ) ) . toBeTruthy ( ) ;
756816 expect ( screen . getByText ( "CI failure on PR #42" ) ) . toBeTruthy ( ) ;
757817 } ) ;
818+
819+ it ( "migrates legacy persisted workspace id to canonical workspace id" , async ( ) => {
820+ localStorageMock . setItem ( "dexdex-active-workspace-id" , LEGACY_DEFAULT_WORKSPACE_ID ) ;
821+ renderWithProviders ( < App /> ) ;
822+
823+ await screen . findByTestId ( "task-list" ) ;
824+ await waitFor ( ( ) => {
825+ expect ( localStorageMock . getItem ( "dexdex-active-workspace-id" ) ) . toBe ( DEFAULT_WORKSPACE_ID ) ;
826+ } ) ;
827+ } ) ;
828+
829+ it ( "blocks repository creation when no workspace exists" , async ( ) => {
830+ localStorageMock . removeItem ( "dexdex-active-workspace-id" ) ;
831+ renderWithProviders ( < App /> , {
832+ initialEntries : [ "/repositories" ] ,
833+ transportOptions : { workspaces : [ ] } ,
834+ } ) ;
835+
836+ expect ( await screen . findByTestId ( "repositories-page" ) ) . toBeTruthy ( ) ;
837+ expect ( screen . getByTestId ( "repository-workspace-hint" ) ) . toBeTruthy ( ) ;
838+
839+ const createInput = screen . getByTestId ( "create-repository-url" ) as HTMLInputElement ;
840+ const createButton = screen . getByRole ( "button" , { name : "Add Repository" } ) as HTMLButtonElement ;
841+ expect ( createInput . disabled ) . toBe ( true ) ;
842+ expect ( createButton . disabled ) . toBe ( true ) ;
843+ } ) ;
844+
845+ it ( "shows repository create validation errors" , async ( ) => {
846+ const user = userEvent . setup ( ) ;
847+ renderWithProviders ( < App /> , { initialEntries : [ "/repositories" ] } ) ;
848+
849+ expect ( await screen . findByTestId ( "repositories-page" ) ) . toBeTruthy ( ) ;
850+ const createInput = screen . getByTestId ( "create-repository-url" ) as HTMLInputElement ;
851+ const createButton = screen . getByRole ( "button" , { name : "Add Repository" } ) as HTMLButtonElement ;
852+
853+ await waitFor ( ( ) => {
854+ expect ( createInput . disabled ) . toBe ( false ) ;
855+ expect ( createButton . disabled ) . toBe ( false ) ;
856+ } ) ;
857+
858+ await user . clear ( createInput ) ;
859+ await user . type ( createInput , "github.com/example/new-repo" ) ;
860+ await user . click ( createButton ) ;
861+
862+ const mutationError = await screen . findByTestId ( "repository-mutation-error" ) ;
863+ expect ( mutationError . textContent ) . toBe ( "Repository URL must start with http:// or https://." ) ;
864+ } ) ;
865+
866+ it ( "shows repository create mutation errors" , async ( ) => {
867+ const user = userEvent . setup ( ) ;
868+ renderWithProviders ( < App /> , {
869+ initialEntries : [ "/repositories" ] ,
870+ transportOptions : { createRepositoryErrorMessage : "rpc create failed" } ,
871+ } ) ;
872+
873+ expect ( await screen . findByTestId ( "repositories-page" ) ) . toBeTruthy ( ) ;
874+ const createInput = screen . getByTestId ( "create-repository-url" ) as HTMLInputElement ;
875+ const createButton = screen . getByRole ( "button" , { name : "Add Repository" } ) as HTMLButtonElement ;
876+
877+ await waitFor ( ( ) => {
878+ expect ( createInput . disabled ) . toBe ( false ) ;
879+ expect ( createButton . disabled ) . toBe ( false ) ;
880+ } ) ;
881+
882+ await user . type ( createInput , "https://github.com/example/new-repo" ) ;
883+ await user . click ( createButton ) ;
884+
885+ const mutationError = await screen . findByTestId ( "repository-mutation-error" ) ;
886+ expect ( mutationError . textContent ?. includes ( "Failed to add repository:" ) ) . toBe ( true ) ;
887+ expect ( mutationError . textContent ?. includes ( "rpc create failed" ) ) . toBe ( true ) ;
888+ } ) ;
758889} ) ;
0 commit comments