11/**
22 * E2E Tests: Workspaces page.
33 *
4- * A WorkspaceBinding maps a normalized filesystem path to a concrete
5- * (space_id, feature_set_id) pair. Roots are globally unique. These specs
6- * cover the CRUD path plus the UI shell.
4+ * A WorkspaceBinding ("mapping") maps a normalized filesystem path to one or
5+ * more FeatureSets within a Space. Roots are globally unique. These specs
6+ * cover the CRUD path, the UI card render, the manual-Apply create form, and
7+ * duplicate-folder validation.
78 *
89 * Uses data-testid only (ADR-003).
910 */
@@ -12,7 +13,7 @@ import { byTestId, safeClick, TIMEOUT } from '../helpers/selectors';
1213import {
1314 createWorkspaceBinding ,
1415 deleteWorkspaceBinding ,
15- getActiveSpace ,
16+ getDefaultSpace ,
1617 listFeatureSetsBySpace ,
1718 listWorkspaceBindings ,
1819 type WorkspaceBinding ,
@@ -25,10 +26,19 @@ function uniqueRoot(): string {
2526 : `/tmp/mcpmux-e2e-${ stamp } ` ;
2627}
2728
29+ /** First auto-seeded ("starter"/legacy "default") FS in a space, else any. */
30+ async function pickFeatureSet ( spaceId : string ) : Promise < string > {
31+ const fsList = await listFeatureSetsBySpace ( spaceId ) ;
32+ const seed =
33+ fsList . find ( ( fs ) => fs . feature_set_type === 'starter' || fs . feature_set_type === 'default' ) ??
34+ fsList [ 0 ] ;
35+ if ( ! seed ) throw new Error ( 'No FeatureSet in space — cannot set up test' ) ;
36+ return seed . id ;
37+ }
38+
2839describe ( 'Workspaces - Page shell' , ( ) => {
2940 before ( async ( ) => {
30- // Clean any leftover e2e bindings so the empty-state / populated-state
31- // assertions are deterministic across reruns.
41+ // Clean any leftover e2e bindings so the state assertions are deterministic.
3242 const existing = await listWorkspaceBindings ( ) ;
3343 for ( const b of existing . filter ( ( x ) => x . workspace_root . includes ( 'mcpmux-e2e' ) ) ) {
3444 await deleteWorkspaceBinding ( b . id ) ;
@@ -57,29 +67,26 @@ describe('Workspaces - Create, render, delete', () => {
5767 const root = uniqueRoot ( ) ;
5868
5969 before ( async ( ) => {
60- const active = await getActiveSpace ( ) ;
61- if ( ! active ) throw new Error ( 'No active space — cannot set up test' ) ;
62- spaceId = active . id ;
63- const fsList = await listFeatureSetsBySpace ( spaceId ) ;
64- const defaultFs = fsList . find ( ( fs ) => fs . feature_set_type === 'default' ) ;
65- if ( ! defaultFs ) throw new Error ( 'No Default FS in active space' ) ;
66- featureSetId = defaultFs . id ;
70+ const space = await getDefaultSpace ( ) ;
71+ if ( ! space ) throw new Error ( 'No default space — cannot set up test' ) ;
72+ spaceId = space . id ;
73+ featureSetId = await pickFeatureSet ( spaceId ) ;
6774 } ) ;
6875
69- it ( 'TC-WS-002: Create binding pointing at the active space default FS' , async ( ) => {
76+ it ( 'TC-WS-002: Create mapping pointing at the default space FS' , async ( ) => {
7077 const created : WorkspaceBinding = await createWorkspaceBinding ( {
7178 workspace_root : root ,
7279 space_id : spaceId ,
73- feature_set_id : featureSetId ,
80+ feature_set_ids : [ featureSetId ] ,
7481 } ) ;
7582 bindingId = created . id ;
7683
7784 expect ( created . workspace_root . toLowerCase ( ) . endsWith ( root . toLowerCase ( ) ) ) . toBe ( true ) ;
7885 expect ( created . space_id ) . toBe ( spaceId ) ;
79- expect ( created . feature_set_id ) . toBe ( featureSetId ) ;
86+ expect ( created . feature_set_ids ) . toContain ( featureSetId ) ;
8087 } ) ;
8188
82- it ( 'TC-WS-003: Binding row renders on the Workspaces page' , async ( ) => {
89+ it ( 'TC-WS-003: Mapping card renders on the Workspaces page' , async ( ) => {
8390 const nav = await byTestId ( 'nav-workspaces' ) ;
8491 await safeClick ( nav ) ;
8592 await browser . pause ( 1500 ) ;
@@ -94,22 +101,21 @@ describe('Workspaces - Create, render, delete', () => {
94101 await browser . saveScreenshot ( './tests/e2e/screenshots/ws-02-populated.png' ) ;
95102
96103 if ( bindingId ) {
97- const row = await $ ( `[data-testid="workspace-binding-row -${ bindingId } "]` ) ;
98- await row . waitForDisplayed ( { timeout : TIMEOUT . short } ) ;
99- expect ( await row . isDisplayed ( ) ) . toBe ( true ) ;
104+ const card = await $ ( `[data-testid="workspace-entry -${ bindingId } "]` ) ;
105+ await card . waitForDisplayed ( { timeout : TIMEOUT . short } ) ;
106+ expect ( await card . isDisplayed ( ) ) . toBe ( true ) ;
100107 }
101108 } ) ;
102109
103- it ( 'TC-WS-004: Binding row references the target Space + FS by name' , async ( ) => {
110+ it ( 'TC-WS-004: Card references the target Space by name' , async ( ) => {
104111 const src = await browser . getPageSource ( ) ;
105- // The row's footer shows "Routes to <FS> in <Space>" — check the Space
106- // name is present. FS is "Default" (builtin) which may also appear in
107- // unrelated copy, so we only assert on the Space name for stability.
108- const active = await getActiveSpace ( ) ;
109- expect ( src . includes ( active ?. name ?? '__never__' ) ) . toBe ( true ) ;
112+ // The card footer shows "Serves <FS> from <Space>" — check the Space name
113+ // is present (FS names may collide with unrelated copy).
114+ const space = await getDefaultSpace ( ) ;
115+ expect ( src . includes ( space ?. name ?? '__never__' ) ) . toBe ( true ) ;
110116 } ) ;
111117
112- it ( 'TC-WS-005: Delete binding and row disappears' , async ( ) => {
118+ it ( 'TC-WS-005: Delete mapping and card disappears' , async ( ) => {
113119 if ( ! bindingId ) throw new Error ( 'bindingId missing — TC-WS-002 must succeed first' ) ;
114120 await deleteWorkspaceBinding ( bindingId ) ;
115121
@@ -120,8 +126,8 @@ describe('Workspaces - Create, render, delete', () => {
120126 await safeClick ( nav ) ;
121127 await browser . pause ( 1500 ) ;
122128
123- const rows = await $$ ( `[data-testid="workspace-binding-row -${ bindingId } "]` ) ;
124- expect ( rows . length ) . toBe ( 0 ) ;
129+ const cards = await $$ ( `[data-testid="workspace-entry -${ bindingId } "]` ) ;
130+ expect ( cards . length ) . toBe ( 0 ) ;
125131 bindingId = null ;
126132 } ) ;
127133
@@ -139,7 +145,7 @@ describe('Workspaces - Create, render, delete', () => {
139145describe ( 'Workspaces - Create form flow (UI)' , ( ) => {
140146 let bindingId : string | null = null ;
141147
142- it ( 'TC-WS-006: Create binding through the form and see it listed' , async ( ) => {
148+ it ( 'TC-WS-006: Create mapping through the form and see it listed' , async ( ) => {
143149 const nav = await byTestId ( 'nav-workspaces' ) ;
144150 await safeClick ( nav ) ;
145151 await browser . pause ( 1000 ) ;
@@ -151,27 +157,58 @@ describe('Workspaces - Create form flow (UI)', () => {
151157 const rootInput = await byTestId ( 'workspace-binding-root-input' ) ;
152158 const root = uniqueRoot ( ) ;
153159 await rootInput . setValue ( root ) ;
160+ // Let the debounced root validation + default FS auto-select settle.
161+ await browser . pause ( 600 ) ;
154162
155- // `space` and `fs` default to the active space + its Default FS, so we
156- // can submit without touching the pickers.
163+ // Space defaults to the default space + its starter FS is auto-selected,
164+ // so the explicit Apply ("Create mapping") can be pressed without touching
165+ // the pickers.
157166 const submit = await byTestId ( 'workspace-binding-submit' ) ;
167+ await submit . waitForEnabled ( { timeout : TIMEOUT . short } ) ;
158168 await safeClick ( submit ) ;
159169 await browser . pause ( 800 ) ;
160170
161- const created = ( await listWorkspaceBindings ( ) ) . find (
162- ( b ) => b . workspace_root . toLowerCase ( ) . endsWith ( root . toLowerCase ( ) )
171+ const created = ( await listWorkspaceBindings ( ) ) . find ( ( b ) =>
172+ b . workspace_root . toLowerCase ( ) . endsWith ( root . toLowerCase ( ) )
163173 ) ;
164174 expect ( created ) . toBeTruthy ( ) ;
165175 if ( created ) {
166176 bindingId = created . id ;
167- const row = await $ ( `[data-testid="workspace-binding-row -${ created . id } "]` ) ;
168- await row . waitForDisplayed ( { timeout : TIMEOUT . short } ) ;
169- expect ( await row . isDisplayed ( ) ) . toBe ( true ) ;
177+ const card = await $ ( `[data-testid="workspace-entry -${ created . id } "]` ) ;
178+ await card . waitForDisplayed ( { timeout : TIMEOUT . short } ) ;
179+ expect ( await card . isDisplayed ( ) ) . toBe ( true ) ;
170180 }
171181
172182 await browser . saveScreenshot ( './tests/e2e/screenshots/ws-04-created-via-form.png' ) ;
173183 } ) ;
174184
185+ it ( 'TC-WS-007: Mapping an already-mapped folder shows a duplicate error and blocks Apply' , async ( ) => {
186+ if ( ! bindingId ) throw new Error ( 'bindingId missing — TC-WS-006 must succeed first' ) ;
187+ const existing = ( await listWorkspaceBindings ( ) ) . find ( ( b ) => b . id === bindingId ) ;
188+ if ( ! existing ) throw new Error ( 'expected the TC-WS-006 binding to still exist' ) ;
189+
190+ const nav = await byTestId ( 'nav-workspaces' ) ;
191+ await safeClick ( nav ) ;
192+ await browser . pause ( 800 ) ;
193+
194+ const toggle = await byTestId ( 'workspace-binding-create-toggle' ) ;
195+ await safeClick ( toggle ) ;
196+ await browser . pause ( 400 ) ;
197+
198+ const rootInput = await byTestId ( 'workspace-binding-root-input' ) ;
199+ await rootInput . setValue ( existing . workspace_root ) ;
200+ await browser . pause ( 700 ) ; // debounced validation + duplicate check
201+
202+ const dupError = await byTestId ( 'workspace-binding-duplicate-error' ) ;
203+ await dupError . waitForDisplayed ( { timeout : TIMEOUT . short } ) ;
204+ expect ( await dupError . isDisplayed ( ) ) . toBe ( true ) ;
205+
206+ const submit = await byTestId ( 'workspace-binding-submit' ) ;
207+ expect ( await submit . isEnabled ( ) ) . toBe ( false ) ;
208+
209+ await browser . saveScreenshot ( './tests/e2e/screenshots/ws-05-duplicate.png' ) ;
210+ } ) ;
211+
175212 after ( async ( ) => {
176213 if ( bindingId ) {
177214 try {
0 commit comments