fix(hubble): initialize UI smoke product mode

- load runtime config during API-backed browser authentication
- validate PD and non-PD graphspace landing contracts separately
- cover config injection and failure handling with Node tests
diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.js b/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.js
index 87fc5fc..7ca54f8 100755
--- a/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.js
+++ b/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.js
@@ -131,11 +131,15 @@
     }
   });
 
+  const graphspaceRoute = auth.pdEnabled
+    ? { name: 'graphspace', path: '/graphspace',
+        requiredApis: ['/api/v1.3/graphspaces'],
+        readySelector: '[data-testid="graphspace-page-title"]' }
+    : { name: 'graphspace', path: '/graphspace',
+        requiredApis: ['/api/v1.3/graphspaces/DEFAULT/graphs'],
+        textPattern: /图管理|Graph Management/ };
   const routes = [
-    { name: 'graphspace', path: '/graphspace',
-      requiredApis: ['/api/v1.3/graphspaces'],
-      readySelector: '[data-testid="graphspace-page-title"]',
-      textPattern: /图空间|Graph Space/ },
+    graphspaceRoute,
     { name: 'gremlin', path: '/gremlin',
       requiredApis: ['/api/v1.3/graphspaces/list'],
       textPattern: /Gremlin|图查询|查询/ },
diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/ui_auth.js b/hugegraph-hubble/hubble-dist/assembly/travis/ui_auth.js
index 33a6889..0ca09cd 100644
--- a/hugegraph-hubble/hubble-dist/assembly/travis/ui_auth.js
+++ b/hugegraph-hubble/hubble-dist/assembly/travis/ui_auth.js
@@ -49,10 +49,17 @@
   if (!user || !user.user_name) {
     throw new Error('login response did not include user_name');
   }
-  await page.addInitScript(serverUser => {
-    window.sessionStorage.setItem('user_', JSON.stringify(serverUser));
-  }, user);
-  return {user, level: status.level};
+  const config = await payload(await context.request.get(
+    `${baseUrl}/api/v1.3/config`
+  ), 'config');
+  if (!config || typeof config.pd_enabled !== 'boolean') {
+    throw new Error('config response did not include pd_enabled');
+  }
+  await page.addInitScript(session => {
+    window.sessionStorage.setItem('user_', JSON.stringify(session.user));
+    window.sessionStorage.setItem('hubble_config_', JSON.stringify(session.config));
+  }, {user, config});
+  return {user, level: status.level, pdEnabled: config.pd_enabled};
 }
 
 module.exports = {authenticateUi};
diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/ui_auth.test.js b/hugegraph-hubble/hubble-dist/assembly/travis/ui_auth.test.js
index 4d8ca29..3eb262c 100644
--- a/hugegraph-hubble/hubble-dist/assembly/travis/ui_auth.test.js
+++ b/hugegraph-hubble/hubble-dist/assembly/travis/ui_auth.test.js
@@ -83,20 +83,37 @@
 
 test('uses the server user and shared browser context session', async () => {
   const user = {id: 7, user_name: 'admin', is_superadmin: true};
+  const config = {pd_enabled: false, server_url: 'http://127.0.0.1:8080'};
   const item = fixture([
     response(200, {status: 200, data: user}),
-    response(200, {status: 200, data: {level: 'ADMIN'}})
+    response(200, {status: 200, data: {level: 'ADMIN'}}),
+    response(200, {status: 200, data: config})
   ]);
   const result = await authenticateUi(item.context, item.page,
                                       'http://hubble/', 'admin', 'pa');
 
-  assert.deepEqual(result, {user, level: 'ADMIN'});
+  assert.deepEqual(result, {user, level: 'ADMIN', pdEnabled: false});
   assert.equal(item.calls[0].url, 'http://hubble/api/v1.3/auth/login');
   assert.deepEqual(item.calls[0].options.data, {
     user_name: 'admin',
     user_password: 'pa'
   });
   assert.equal(item.calls[1].url, 'http://hubble/api/v1.3/auth/status');
+  assert.equal(item.calls[2].url, 'http://hubble/api/v1.3/config');
   assert.equal(item.initScripts.length, 1);
-  assert.deepEqual(item.initScripts[0].value, user);
+  assert.deepEqual(item.initScripts[0].value, {user, config});
+});
+
+test('requires runtime config before opening authenticated routes', async () => {
+  const item = fixture([
+    response(200, {status: 200, data: {id: 7, user_name: 'admin'}}),
+    response(200, {status: 200, data: {level: 'ADMIN'}}),
+    response(200, {status: 500, message: 'config unavailable'})
+  ]);
+
+  await assert.rejects(
+    authenticateUi(item.context, item.page, 'http://hubble', 'admin', 'pa'),
+    /config failed: config unavailable/
+  );
+  assert.equal(item.initScripts.length, 0);
 });