dsh-dbhub-live 5.0.1 → 5.2.0-dev.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +95 -209
- package/README.en.md +251 -169
- package/README.md +251 -169
- package/doc/ARCHITECTURE.md +108 -0
- package/doc/CLIENT.md +124 -0
- package/doc/DESENSITIZE.md +56 -0
- package/doc/DEVELOPMENT.md +139 -0
- package/doc/IMPACT-MATRIX.md +36 -0
- package/doc/RELEASE.md +93 -0
- package/doc/REQUIREMENTS.md +177 -105
- package/doc/images/chat-query.png +0 -0
- package/doc/images/row-editor.png +0 -0
- package/doc/images/settings-page.png +0 -0
- package/lib/adhoc.mjs +412 -34
- package/lib/capability.mjs +157 -0
- package/lib/client.js +1243 -80
- package/lib/config.mjs +370 -21
- package/lib/i18n.mjs +119 -4
- package/lib/index.mjs +401 -43
- package/lib/mcp.mjs +496 -391
- package/lib/meta.mjs +267 -0
- package/lib/options.mjs +25 -5
- package/lib/policy.mjs +92 -0
- package/lib/toml.mjs +188 -0
- package/lib/tools.mjs +1093 -827
- package/lib/update.mjs +244 -0
- package/package.json +34 -10
- package/scripts/desensitize-check.mjs +10 -3
- package/scripts/desensitize.config.mjs +255 -237
- package/scripts/docs-check.mjs +214 -0
- package/scripts/release-preflight.mjs +180 -0
- package/scripts/render-check.mjs +1366 -843
- package/test/adhoc-toml.test.mjs +365 -0
- package/test/bridge.test.mjs +227 -185
- package/test/capability.test.mjs +231 -0
- package/test/client-format.test.mjs +515 -352
- package/test/configure-options.test.mjs +244 -0
- package/test/configure-ssh.test.mjs +232 -0
- package/test/copy-from.test.mjs +155 -0
- package/test/env-options.test.mjs +456 -0
- package/test/meta.test.mjs +234 -0
- package/test/options.test.mjs +160 -123
- package/test/policy.test.mjs +132 -0
- package/test/row-cache.test.mjs +237 -138
- package/test/ssh-probe.test.mjs +834 -0
- package/test/test-op-fastpath.test.mjs +208 -0
- package/test/toml.test.mjs +352 -0
- package/test/update.test.mjs +270 -0
- package/test/util.test.mjs +17 -0
- package/test/zero-knowledge.test.mjs +120 -78
|
@@ -1,352 +1,515 @@
|
|
|
1
|
-
// Contract check for the hand-authored client bundle: the file must be a
|
|
2
|
-
// lazy-CJS registration the client module loader can serve and materialize,
|
|
3
|
-
// and it must keep the page layout contract (grouped accordion / flat tiles),
|
|
4
|
-
// the zero-knowledge surface and the configOp state machine intact.
|
|
5
|
-
|
|
6
|
-
import test from 'node:test'
|
|
7
|
-
import assert from 'node:assert/strict'
|
|
8
|
-
import { readFileSync } from 'node:fs'
|
|
9
|
-
import { fileURLToPath } from 'node:url'
|
|
10
|
-
import { dirname, join } from 'node:path'
|
|
11
|
-
|
|
12
|
-
const here = dirname(fileURLToPath(import.meta.url))
|
|
13
|
-
const source = readFileSync(join(here, '..', 'lib', 'client.js'), 'utf8')
|
|
14
|
-
|
|
15
|
-
test('client bundle registers through window.__ModuleLoader__.load', () => {
|
|
16
|
-
assert.match(source, /window\.__ModuleLoader__\.load\(\{\s*id:\s*"dsh-dbhub-live"/)
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
test('client bundle factory is lazy CJS with module/exports boilerplate', () => {
|
|
20
|
-
assert.match(source, /factory:\s*\(require\)\s*=>\s*\{/)
|
|
21
|
-
assert.match(source, /var module = \{ exports: \{\} \};/)
|
|
22
|
-
assert.match(source, /Object\.defineProperty\(exports, Symbol\.toStringTag, \{ value: "Module" \}\);/)
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
test('client bundle requires only baseline platform modules', () => {
|
|
26
|
-
const requires = [...source.matchAll(/require\(\s*"([^"]+)"\s*\)/g)].map((m) => m[1])
|
|
27
|
-
assert.deepEqual(requires, ['react', '@deepseek-ai/dsh-client-ui-primitives'])
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
test('client bundle exports name/inject/apply and returns module.exports', () => {
|
|
31
|
-
assert.match(source, /exports\.name = name;/)
|
|
32
|
-
assert.match(source, /exports\.inject = inject;/)
|
|
33
|
-
assert.match(source, /exports\.apply = apply;/)
|
|
34
|
-
assert.match(source, /return module\.exports;/)
|
|
35
|
-
// The transport is PROBED, never injected: a `settingsScope` injection is
|
|
36
|
-
// unsatisfiable on dsh 0.2.x and keeps the whole browser half pending (the
|
|
37
|
-
// page, the sidebar entry and the Plugins card all vanish silently).
|
|
38
|
-
assert.match(source, /var inject = \["slots"\];/)
|
|
39
|
-
assert.doesNotMatch(source, /var inject = \[[^\]]*settingsScope/)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('client bundle binds its host face through the runtime ladder', () => {
|
|
43
|
-
// 0.1.x keeps the settings namespace; every newer line reads the plugin's own
|
|
44
|
-
// authenticated bridge instead.
|
|
45
|
-
assert.match(source, /function legacyFace\(scope\)/)
|
|
46
|
-
assert.match(source, /function httpFace\(\)/)
|
|
47
|
-
assert.match(source, /var BRIDGE_BASE = "api\/dsh-dbhub-live";/)
|
|
48
|
-
assert.match(source, /var legacyScope = typeof ctx\.get === "function" \? ctx\.get\("settingsScope"\) : undefined;/)
|
|
49
|
-
assert.match(source, /var face = legacyScope && typeof legacyScope\.bind === "function" \? legacyFace\(legacyScope\) : httpFace\(\);/)
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
test('client bundle writes options through the settings form when it can', () => {
|
|
53
|
-
// dsh 0.1.7+/0.2.x: `ctx.configForms.get(entryId).set(field, value)` — the
|
|
54
|
-
// entry id arrives in the Host view. POST /options is the fallback (0.1.x, or
|
|
55
|
-
// a deployment without the settings client).
|
|
56
|
-
assert.match(source, /var saveOptions = function \(patch\) \{/)
|
|
57
|
-
assert.match(source, /var forms = typeof ctx\.get === "function" \? ctx\.get\("configForms"\) : undefined;/)
|
|
58
|
-
assert.match(source, /form = forms\.get\(entryId\);/)
|
|
59
|
-
assert.match(source, /return form\.set\(key, raw\)\.then/)
|
|
60
|
-
assert.match(source, /return post\("\/options", patch\);/)
|
|
61
|
-
assert.match(source, /saveConfig: function \(patch\) \{ return saveOptions\(patch \|\| \{\}\); \},/)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
test('client bundle registers the plugins.row.config page keyed to package#row', () => {
|
|
65
|
-
// dsh 0.1.6 dropped settings.plugin.item; the plugin's own configuration now
|
|
66
|
-
// lives on the Plugins page through the keyed plugins.row.config slot.
|
|
67
|
-
assert.doesNotMatch(source, /settings\.plugin\.item/)
|
|
68
|
-
assert.match(source, /slots\.inject\(SLOT/)
|
|
69
|
-
assert.match(source, /var SLOT = "plugins\.row\.config";/)
|
|
70
|
-
assert.match(source, /var SLOT_KEY = "dsh-dbhub-live#dbhub-live";/)
|
|
71
|
-
assert.match(source, /name:\s*SLOT, key:\s*SLOT_KEY/)
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
test('client bundle renders both owner views (summary one-liner + page)', () => {
|
|
75
|
-
assert.match(source, /var isPage = props\.view !== "summary";/)
|
|
76
|
-
assert.match(source, /if \(!isPage\)/)
|
|
77
|
-
assert.match(source, /h\("span", \{ className: "dbh-summary" \}/)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
test('client bundle owns a Settings section (core shell, always available)', () => {
|
|
81
|
-
assert.match(source, /slots\.inject\("settings\.section"/)
|
|
82
|
-
assert.match(source, /name: "settings\.section",\s*\n\s*id: "dbhub"/)
|
|
83
|
-
assert.match(source, /label: function \(\) \{ return t\("title"\); \}/)
|
|
84
|
-
assert.match(source, /function ConfigPanel\(props\)/)
|
|
85
|
-
// the settings section owns its scroll container; the Plugins row does not
|
|
86
|
-
assert.match(source, /className: "dbh-wrap"/)
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
test('client bundle owns
|
|
90
|
-
assert.match(source, /slots\.inject\("sidebar\.panellist"/)
|
|
91
|
-
assert.match(source, /name: "sidebar\.panellist",\s*\n\s*id: "dbhub"/)
|
|
92
|
-
assert.match(source, /function PanelIcon\(props\)/)
|
|
93
|
-
assert.match(source, /slots\.inject\("main"/)
|
|
94
|
-
assert.match(source, /name: "main",\s*\n\s*key: "dbhub"/)
|
|
95
|
-
// the
|
|
96
|
-
|
|
97
|
-
assert.match(source, /
|
|
98
|
-
assert.match(source, /
|
|
99
|
-
assert.match(source, /
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
assert.match(source, /
|
|
104
|
-
assert.match(source, /
|
|
105
|
-
assert.match(source,
|
|
106
|
-
// the
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
assert.match(source, /
|
|
115
|
-
assert.match(source, /
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
assert.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
assert.match(source,
|
|
123
|
-
assert.match(source,
|
|
124
|
-
assert.match(source,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
assert.match(source, /
|
|
131
|
-
assert.match(source, /
|
|
132
|
-
assert.match(source, /
|
|
133
|
-
assert.match(source, /
|
|
134
|
-
assert.match(source, /
|
|
135
|
-
assert.match(source, /
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
assert.match(source, /
|
|
142
|
-
assert.match(source, /
|
|
143
|
-
assert.match(source, /
|
|
144
|
-
assert.match(source, /
|
|
145
|
-
|
|
146
|
-
assert.match(source, /
|
|
147
|
-
assert.match(source, /
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
assert.match(source, /
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
assert.match(source, /
|
|
156
|
-
|
|
157
|
-
assert.match(source, /groups\.
|
|
158
|
-
|
|
159
|
-
assert.match(source, /
|
|
160
|
-
assert.match(source, /
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
assert.match(source, /
|
|
167
|
-
assert.match(source, /
|
|
168
|
-
assert.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
assert.match(source, /
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
assert.match(source, /var
|
|
176
|
-
assert.match(source, /
|
|
177
|
-
assert.match(source, /"
|
|
178
|
-
assert.match(source, /
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
assert.match(source, /
|
|
190
|
-
|
|
191
|
-
assert.match(source, /
|
|
192
|
-
assert.match(source, /
|
|
193
|
-
assert.match(source, /
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
assert.match(source, /
|
|
207
|
-
|
|
208
|
-
assert.match(source, /
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
assert.match(source, /function
|
|
218
|
-
assert.match(source, /
|
|
219
|
-
assert.match(source, /
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
assert.match(source, /
|
|
225
|
-
assert.match(source, /
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
assert.match(source, /
|
|
233
|
-
assert.match(source, /
|
|
234
|
-
assert.match(source, /
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
assert.match(source, /
|
|
239
|
-
assert.match(source, /var
|
|
240
|
-
// the
|
|
241
|
-
assert.match(source, /
|
|
242
|
-
assert.match(source, /
|
|
243
|
-
assert.match(source, /
|
|
244
|
-
|
|
245
|
-
assert.match(source, /
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
assert.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
assert.match(source, /
|
|
256
|
-
assert.match(source, /
|
|
257
|
-
|
|
258
|
-
assert.match(source, /
|
|
259
|
-
|
|
260
|
-
assert.match(source, /
|
|
261
|
-
assert.match(source, /"
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
assert.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
assert.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
assert.
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
assert.
|
|
308
|
-
|
|
309
|
-
assert.match(
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
assert.match(source, /
|
|
317
|
-
|
|
318
|
-
assert.match(source, /
|
|
319
|
-
assert.match(source, /
|
|
320
|
-
assert.match(source, /
|
|
321
|
-
|
|
322
|
-
assert.match(source, /
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
assert.match(source, /
|
|
329
|
-
|
|
330
|
-
assert.match(source, /
|
|
331
|
-
assert.match(source, /
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
assert.match(source, /
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
assert.match(source, /className: "dbh-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
assert.match(source, /
|
|
351
|
-
assert.match(source, /
|
|
352
|
-
|
|
1
|
+
// Contract check for the hand-authored client bundle: the file must be a
|
|
2
|
+
// lazy-CJS registration the client module loader can serve and materialize,
|
|
3
|
+
// and it must keep the page layout contract (grouped accordion / flat tiles),
|
|
4
|
+
// the zero-knowledge surface and the configOp state machine intact.
|
|
5
|
+
|
|
6
|
+
import test from 'node:test'
|
|
7
|
+
import assert from 'node:assert/strict'
|
|
8
|
+
import { readFileSync } from 'node:fs'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
import { dirname, join } from 'node:path'
|
|
11
|
+
|
|
12
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
13
|
+
const source = readFileSync(join(here, '..', 'lib', 'client.js'), 'utf8')
|
|
14
|
+
|
|
15
|
+
test('client bundle registers through window.__ModuleLoader__.load', () => {
|
|
16
|
+
assert.match(source, /window\.__ModuleLoader__\.load\(\{\s*id:\s*"dsh-dbhub-live"/)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('client bundle factory is lazy CJS with module/exports boilerplate', () => {
|
|
20
|
+
assert.match(source, /factory:\s*\(require\)\s*=>\s*\{/)
|
|
21
|
+
assert.match(source, /var module = \{ exports: \{\} \};/)
|
|
22
|
+
assert.match(source, /Object\.defineProperty\(exports, Symbol\.toStringTag, \{ value: "Module" \}\);/)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('client bundle requires only baseline platform modules', () => {
|
|
26
|
+
const requires = [...source.matchAll(/require\(\s*"([^"]+)"\s*\)/g)].map((m) => m[1])
|
|
27
|
+
assert.deepEqual(requires, ['react', '@deepseek-ai/dsh-client-ui-primitives'])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('client bundle exports name/inject/apply and returns module.exports', () => {
|
|
31
|
+
assert.match(source, /exports\.name = name;/)
|
|
32
|
+
assert.match(source, /exports\.inject = inject;/)
|
|
33
|
+
assert.match(source, /exports\.apply = apply;/)
|
|
34
|
+
assert.match(source, /return module\.exports;/)
|
|
35
|
+
// The transport is PROBED, never injected: a `settingsScope` injection is
|
|
36
|
+
// unsatisfiable on dsh 0.2.x and keeps the whole browser half pending (the
|
|
37
|
+
// page, the sidebar entry and the Plugins card all vanish silently).
|
|
38
|
+
assert.match(source, /var inject = \["slots"\];/)
|
|
39
|
+
assert.doesNotMatch(source, /var inject = \[[^\]]*settingsScope/)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('client bundle binds its host face through the runtime ladder', () => {
|
|
43
|
+
// 0.1.x keeps the settings namespace; every newer line reads the plugin's own
|
|
44
|
+
// authenticated bridge instead.
|
|
45
|
+
assert.match(source, /function legacyFace\(scope\)/)
|
|
46
|
+
assert.match(source, /function httpFace\(\)/)
|
|
47
|
+
assert.match(source, /var BRIDGE_BASE = "api\/dsh-dbhub-live";/)
|
|
48
|
+
assert.match(source, /var legacyScope = typeof ctx\.get === "function" \? ctx\.get\("settingsScope"\) : undefined;/)
|
|
49
|
+
assert.match(source, /var face = legacyScope && typeof legacyScope\.bind === "function" \? legacyFace\(legacyScope\) : httpFace\(\);/)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('client bundle writes options through the settings form when it can', () => {
|
|
53
|
+
// dsh 0.1.7+/0.2.x: `ctx.configForms.get(entryId).set(field, value)` — the
|
|
54
|
+
// entry id arrives in the Host view. POST /options is the fallback (0.1.x, or
|
|
55
|
+
// a deployment without the settings client).
|
|
56
|
+
assert.match(source, /var saveOptions = function \(patch\) \{/)
|
|
57
|
+
assert.match(source, /var forms = typeof ctx\.get === "function" \? ctx\.get\("configForms"\) : undefined;/)
|
|
58
|
+
assert.match(source, /form = forms\.get\(entryId\);/)
|
|
59
|
+
assert.match(source, /return form\.set\(key, raw\)\.then/)
|
|
60
|
+
assert.match(source, /return post\("\/options", patch\);/)
|
|
61
|
+
assert.match(source, /saveConfig: function \(patch\) \{ return saveOptions\(patch \|\| \{\}\); \},/)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('client bundle registers the plugins.row.config page keyed to package#row', () => {
|
|
65
|
+
// dsh 0.1.6 dropped settings.plugin.item; the plugin's own configuration now
|
|
66
|
+
// lives on the Plugins page through the keyed plugins.row.config slot.
|
|
67
|
+
assert.doesNotMatch(source, /settings\.plugin\.item/)
|
|
68
|
+
assert.match(source, /slots\.inject\(SLOT/)
|
|
69
|
+
assert.match(source, /var SLOT = "plugins\.row\.config";/)
|
|
70
|
+
assert.match(source, /var SLOT_KEY = "dsh-dbhub-live#dbhub-live";/)
|
|
71
|
+
assert.match(source, /name:\s*SLOT, key:\s*SLOT_KEY/)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('client bundle renders both owner views (summary one-liner + page)', () => {
|
|
75
|
+
assert.match(source, /var isPage = props\.view !== "summary";/)
|
|
76
|
+
assert.match(source, /if \(!isPage\)/)
|
|
77
|
+
assert.match(source, /h\("span", \{ className: "dbh-summary" \}/)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('client bundle owns a Settings section (core shell, always available)', () => {
|
|
81
|
+
assert.match(source, /slots\.inject\("settings\.section"/)
|
|
82
|
+
assert.match(source, /name: "settings\.section",\s*\n\s*id: "dbhub"/)
|
|
83
|
+
assert.match(source, /label: function \(\) \{ return t\("title"\); \}/)
|
|
84
|
+
assert.match(source, /function ConfigPanel\(props\)/)
|
|
85
|
+
// the settings section owns its scroll container; the Plugins row does not
|
|
86
|
+
assert.match(source, /className: "dbh-wrap"/)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('client bundle owns a sidebar entry gated by enabled AND the user switch', () => {
|
|
90
|
+
assert.match(source, /slots\.inject\("sidebar\.panellist"/)
|
|
91
|
+
assert.match(source, /name: "sidebar\.panellist",\s*\n\s*id: "dbhub"/)
|
|
92
|
+
assert.match(source, /function PanelIcon\(props\)/)
|
|
93
|
+
assert.match(source, /slots\.inject\("main"/)
|
|
94
|
+
assert.match(source, /name: "main",\s*\n\s*key: "dbhub"/)
|
|
95
|
+
// Two independent conditions: the user's "hide the shortcut" switch and the
|
|
96
|
+
// plugin's enabled state. Hiding the entry is NOT disabling the plugin.
|
|
97
|
+
assert.match(source, /return v\.enabled !== false && v\.showSidebarEntry !== false;/)
|
|
98
|
+
assert.match(source, /var syncSidebar = function/)
|
|
99
|
+
assert.match(source, /face\.subscribe\(syncSidebar\)/)
|
|
100
|
+
// The switch itself is back on the settings strip and applies immediately.
|
|
101
|
+
assert.match(source, /"cfg\.sidebar": "在侧边栏显示入口"/)
|
|
102
|
+
assert.match(source, /"cfg\.sidebar": "Show the sidebar entry"/)
|
|
103
|
+
assert.match(source, /var toggleSidebar = function \(next\)/)
|
|
104
|
+
assert.match(source, /props\.saveConfig\(\{ showSidebarEntry: next \}\);/)
|
|
105
|
+
assert.match(source, /showSidebarEntry: draft\.showSidebarEntry !== false,/)
|
|
106
|
+
// The settings section and the Plugins row never depend on either condition, so
|
|
107
|
+
// no combination can lock the user out of the configuration.
|
|
108
|
+
assert.match(source, /slots\.inject\("settings\.section"/)
|
|
109
|
+
assert.match(source, /"cfg\.disabledHint":/)
|
|
110
|
+
assert.match(source, /t\("cfg\.enabledHint"\) : t\("cfg\.disabledHint"\)\) \+ " " \+ t\("cfg\.sidebarHint"\)/)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('client bundle exposes the config editor (saveConfig + editable options)', () => {
|
|
114
|
+
assert.match(source, /saveConfig:\s*function/)
|
|
115
|
+
assert.match(source, /updateIntervalDays/)
|
|
116
|
+
assert.match(source, /保存配置/)
|
|
117
|
+
// the auto-install package is NOT a settings field anymore
|
|
118
|
+
assert.doesNotMatch(source, /dbhubPackage/)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('client bundle injects one scoped stylesheet built on theme tokens', () => {
|
|
122
|
+
assert.match(source, /var STYLE_ID = "dsh-dbhub-live-style";/)
|
|
123
|
+
assert.match(source, /function ensureStyle\(\)/)
|
|
124
|
+
assert.match(source, /document\.getElementById\(STYLE_ID\)/)
|
|
125
|
+
assert.match(source, /document\.createElement\("style"\)/)
|
|
126
|
+
assert.match(source, /ensureStyle\(\);/)
|
|
127
|
+
// tokens with neutral fallbacks so the page follows the active skin/theme
|
|
128
|
+
assert.match(source, /--dsw-alias-bg-layer-2/)
|
|
129
|
+
assert.match(source, /--dsw-alias-label-primary/)
|
|
130
|
+
assert.match(source, /--dsw-alias-interactive-bg-hover/)
|
|
131
|
+
assert.match(source, /--dsw-alias-state-error-primary/)
|
|
132
|
+
assert.match(source, /--dsw-alias-state-success-tertiary/)
|
|
133
|
+
assert.match(source, /--dsw-alias-border-l1/)
|
|
134
|
+
assert.match(source, /\.dbh-btn:focus-visible/)
|
|
135
|
+
assert.match(source, /\.dbh-grouphead:hover/)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('client bundle decides the layout from the workspace×environment shape', () => {
|
|
139
|
+
// Pure helpers: grouping by PATH (a title is not identity) and the
|
|
140
|
+
// threshold-free mode decision (tiles when every workspace has one env).
|
|
141
|
+
assert.match(source, /function groupRows\(rows\)/)
|
|
142
|
+
assert.match(source, /function layoutModeOf\(groups\)/)
|
|
143
|
+
assert.match(source, /if \(groups\[i\]\.envs\.length > 1\) return "grouped";/)
|
|
144
|
+
assert.match(source, /return "tiled";/)
|
|
145
|
+
assert.match(source, /return "empty";/)
|
|
146
|
+
assert.match(source, /var key = String\(r\.path \|\| r\.title \|\| ""\);/)
|
|
147
|
+
assert.match(source, /order\[j\]\.dup = titleCount\[order\[j\]\.title\] > 1;/)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('client bundle folds workspaces per group, with click-to-expand groups', () => {
|
|
151
|
+
assert.match(source, /var groupView = function \(g\)/)
|
|
152
|
+
assert.match(source, /var toggleGroup = function \(g\)/)
|
|
153
|
+
assert.match(source, /"aria-expanded": open/)
|
|
154
|
+
assert.match(source, /className: "dbh-chev" \+ \(open \? " open" : ""\)/)
|
|
155
|
+
assert.match(source, /t\("grp\.envs", \{ n: String\(g\.envs\.length\), m: String\(g\.saved\) \}\)/)
|
|
156
|
+
// a lone workspace opens by itself; otherwise the most recently used one does
|
|
157
|
+
assert.match(source, /if \(groups\.length === 1\) return true;/)
|
|
158
|
+
assert.match(source, /function recentWorkspacePath\(snapshot\)/)
|
|
159
|
+
assert.match(source, /return !!recentPath && samePath\(g\.path, recentPath\);/)
|
|
160
|
+
assert.match(source, /typeof props\.useWorkspaces === "function"/)
|
|
161
|
+
// expand/collapse-all is always available in grouped mode
|
|
162
|
+
assert.match(source, /allOpen \? t\("btn\.collapseAll"\) : t\("btn\.expandAll"\)/)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('client bundle tiles single-environment workspaces instead of folding them', () => {
|
|
166
|
+
assert.match(source, /var tileView = function \(g\)/)
|
|
167
|
+
assert.match(source, /className: "dbh-tiles"/)
|
|
168
|
+
assert.match(source, /groups\.map\(tileView\)/)
|
|
169
|
+
// one row per connection: env chip + 🔒 metadata + actions
|
|
170
|
+
assert.match(source, /var envRow = function \(w\)/)
|
|
171
|
+
assert.match(source, /className: "dbh-envrow"/)
|
|
172
|
+
assert.match(source, /className: "dbh-conn", title: w\.conn/)
|
|
173
|
+
// where a connection came from is identical on almost every row, so it is a
|
|
174
|
+
// tooltip on the environment chip — never an inline badge
|
|
175
|
+
assert.match(source, /var originTitle = function \(w, base\)/)
|
|
176
|
+
assert.match(source, /title: originTitle\(w, w\.env\)/)
|
|
177
|
+
assert.match(source, /t\("ws\.source"\) \+ ":" \+ sourceLabelOf\(w, t\)/)
|
|
178
|
+
assert.match(source, /if \(!w\.persisted\) text \+= " " \+ t\("ws\.autoHint"\);/)
|
|
179
|
+
// the read-only flag is a per-row ATTRIBUTE that changes what a query may do,
|
|
180
|
+
// so it is visible on the chip (the source is a tooltip, this is not)
|
|
181
|
+
assert.match(source, /var envChip = function \(w\)/)
|
|
182
|
+
assert.match(source, /className: "dbh-ro", title: t\("ro\.title"\)/)
|
|
183
|
+
assert.match(source, /if \(w\.ro === true\) text \+= " · " \+ t\("ro\.title"\);/)
|
|
184
|
+
assert.match(source, /if \(w\.ssh\) text \+= " · " \+ t\("ro\.sshTitle"/)
|
|
185
|
+
assert.doesNotMatch(source, /dbh-pill/)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('client bundle localizes copy through ctx.locale (zh/en dictionaries)', () => {
|
|
189
|
+
assert.match(source, /localeSvc\.register\(NS, \{ zh: LOCALE_ZH, en: LOCALE_EN \}\)/)
|
|
190
|
+
assert.match(source, /face\.t = t|t: t,/)
|
|
191
|
+
assert.match(source, /var LOCALE_ZH = \{/)
|
|
192
|
+
assert.match(source, /var LOCALE_EN = \{/)
|
|
193
|
+
assert.match(source, /"conn\.title": "连接"/)
|
|
194
|
+
assert.match(source, /"conn\.title": "Connections"/)
|
|
195
|
+
assert.match(source, /"btn\.addEnv": "+ 环境"/)
|
|
196
|
+
assert.match(source, /"btn\.addEnv": "+ Environment"/)
|
|
197
|
+
assert.match(source, /"cfg\.summary":/)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('client bundle releases its locale dictionaries on disposal (hot reload)', () => {
|
|
201
|
+
// The locale service refuses a namespace+locale it already owns, so a bundle
|
|
202
|
+
// reload that kept its registration alive used to abort the whole plugin half
|
|
203
|
+
// ("locale namespace dsh-dbhub-live already has locale zh").
|
|
204
|
+
assert.match(source, /var localeDispose = null;/)
|
|
205
|
+
assert.match(source, /localeDispose = localeSvc\.register\(NS, \{ zh: LOCALE_ZH, en: LOCALE_EN \}\);/)
|
|
206
|
+
assert.match(source, /ctx\.effect\(function \(\) \{ return localeDispose; \}, "dsh-dbhub-live: locale dictionaries"\);/)
|
|
207
|
+
// ... and a duplicate must degrade to the local dictionaries, never throw.
|
|
208
|
+
assert.match(source, /locale namespace busy, falling back to local dictionaries/)
|
|
209
|
+
assert.match(source, /function localTranslator\(localeSvc\)/)
|
|
210
|
+
assert.match(source, /var t = localeOwned && localeSvc \? localeSvc\.bind\(NS\) : localTranslator\(localeSvc\);/)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test('client bundle hands a copied connection label over verbatim', () => {
|
|
214
|
+
// Copying across the split label's flex boxes makes the browser insert a
|
|
215
|
+
// separator ("…3306/ app" when pasted into a DSN field), so the label answers
|
|
216
|
+
// the copy event itself with the exact metadata string.
|
|
217
|
+
assert.match(source, /var onConnCopy = function \(w, e\)/)
|
|
218
|
+
assert.match(source, /e\.clipboardData\.setData\("text\/plain", String\(w\.conn \|\| ""\)\)/)
|
|
219
|
+
assert.match(source, /h\("span", \{ className: "dbh-conn", title: w\.conn, onCopy: function \(e\) \{ onConnCopy\(w, e\); \} \}/)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
test('client bundle cannot dispatch a second pre-test while one is pending', () => {
|
|
223
|
+
assert.match(source, /var pendingPretest = !!\(pretest && pretest\.pendingNonce\);/)
|
|
224
|
+
assert.match(source, /disabled: pendingPretest,\s*\n\s*onClick: addDsn,/)
|
|
225
|
+
assert.match(source, /disabled: pendingPretest,\s*\n\s*onClick: function \(\) \{ saveEdit\(w\); \},/)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
test('client bundle asks before overwriting an existing environment', () => {
|
|
229
|
+
// Writing into (or renaming onto) an environment that already has a connection
|
|
230
|
+
// REPLACES it — the Host's setWorkspaceEnv is an unconditional store write, so
|
|
231
|
+
// the page must ask before dispatching anything (it used to be silent).
|
|
232
|
+
assert.match(source, /var ENV_NAME_MAX = 64;/)
|
|
233
|
+
assert.match(source, /function normalizeEnvForCompare\(name\)/)
|
|
234
|
+
assert.match(source, /function envCollision\(rows, wsRef, envName\)/)
|
|
235
|
+
assert.match(source, /var requestWrite = function \(form, check\)/)
|
|
236
|
+
assert.match(source, /if \(hit\) \{\s*\n\s*setOverwrite\(\{ form: form, hit: hit \}\);/)
|
|
237
|
+
assert.match(source, /var executeWrite = function \(form\)/)
|
|
238
|
+
assert.match(source, /var confirmOverwrite = function \(\)/)
|
|
239
|
+
assert.match(source, /var cancelOverwrite = function \(\) \{ setOverwrite\(null\); \}/)
|
|
240
|
+
// both entry points go through the gate: the add form and a renaming edit
|
|
241
|
+
assert.match(source, /function connDraftOf\(form\)/)
|
|
242
|
+
assert.match(source, /requestWrite\(withConnDraft\(\{\s*\n\s*ws: addTarget \|\| addForm\.ws\.trim\(\),\s*\n\s*env: addForm\.env\.trim\(\) \|\| "default",\s*\n\s*dsn: addForm\.dsn\.trim\(\),\s*\n\s*_draft: addForm,\s*\n\s*\}, addForm\), true\);/)
|
|
243
|
+
assert.match(source, /_renameFrom: renaming \? row\.env : "" \}, editing\), renaming\);/)
|
|
244
|
+
// editing the connection of the SAME environment is not an overwrite question
|
|
245
|
+
assert.match(source, /var renaming = normalizeEnvForCompare\(newEnv\) !== normalizeEnvForCompare\(row\.env\);/)
|
|
246
|
+
// the prompt is a warning card with an explicit overwrite/cancel pair
|
|
247
|
+
assert.match(source, /className: "dbh-warncard", role: "alert"/)
|
|
248
|
+
assert.match(source, /t\("ovr\.title"\)/)
|
|
249
|
+
assert.match(source, /t\("ovr\.bodyAmbiguous", \{/)
|
|
250
|
+
assert.match(source, /t\("ovr\.confirm"\)/)
|
|
251
|
+
assert.match(source, /overwriteView \|\| pretestView,/)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
test('client bundle keeps the source handle copyable instead of echoing it inline', () => {
|
|
255
|
+
assert.match(source, /w\.srcId/)
|
|
256
|
+
assert.match(source, /var copySource = function \(text, key\)/)
|
|
257
|
+
// the platform clipboard helper already falls back to execCommand
|
|
258
|
+
assert.match(source, /writeClipboard/)
|
|
259
|
+
assert.match(source, /function shortHandle\(id\)/)
|
|
260
|
+
assert.match(source, /var sourceChip = function \(w, key\)/)
|
|
261
|
+
assert.match(source, /className: "dbh-src" \+ \(copied \? " dbh-src-ok" : ""\)/)
|
|
262
|
+
assert.match(source, /t\("ws\.srcHint"\)/)
|
|
263
|
+
assert.match(source, /h\("span", \{ className: "dbh-srcid" \}, copied \? t\("copy\.ok"\) : shortHandle\(w\.srcId\)\)/)
|
|
264
|
+
// the handle is metadata, never a DSN
|
|
265
|
+
assert.doesNotMatch(source, /w\.dsn/)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
test('client bundle draws clear row-action icons instead of text labels', () => {
|
|
269
|
+
assert.match(source, /function pickIcon\(name, glyph\)/)
|
|
270
|
+
// Test / edit / delete are explicit 16px SVG shapes (the bundled edit glyph is
|
|
271
|
+
// ambiguous at 1x); copy and close keep their product primitives.
|
|
272
|
+
assert.match(source, /var svgIcon = function \(children\)/)
|
|
273
|
+
assert.match(source, /var RunIcon = svgIcon\(/)
|
|
274
|
+
assert.match(source, /var EditIcon = svgIcon\(/)
|
|
275
|
+
assert.match(source, /var TrashIcon = svgIcon\(/)
|
|
276
|
+
assert.match(source, /pickIcon\("IconCopyOutline16"/)
|
|
277
|
+
assert.match(source, /pickIcon\("IconCloseOutline16"/)
|
|
278
|
+
// icons carry their label in title/aria-label, never as row text
|
|
279
|
+
assert.match(source, /"aria-label": t\("btn\.test"\)/)
|
|
280
|
+
assert.match(source, /"aria-label": t\("btn\.delete"\)/)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
test('client bundle keeps the database name visible when the row is narrow', () => {
|
|
284
|
+
// `type://host:port/` may be truncated; the database name must not be.
|
|
285
|
+
assert.match(source, /function splitConn\(conn\)/)
|
|
286
|
+
assert.match(source, /className: "dbh-conn-pre"/)
|
|
287
|
+
assert.match(source, /className: "dbh-conn-db"/)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
test('client bundle styles stay theme-safe (tokens only, no opaque literals)', () => {
|
|
291
|
+
// A themed or translucent skin may redefine the tokens or leave some
|
|
292
|
+
// undefined: surfaces must inherit the page background instead of painting a
|
|
293
|
+
// hard block, and text must never fall back to a fixed light-mode grey.
|
|
294
|
+
assert.doesNotMatch(source, /#ffffff|#f6f8fa|#d0d7de|#1f2328|#e03131|#2f9e44/i)
|
|
295
|
+
const start = source.indexOf('var CSS_TEXT = [')
|
|
296
|
+
const end = source.indexOf('].join("");', start)
|
|
297
|
+
assert.ok(start > 0 && end > start, 'CSS_TEXT block must exist')
|
|
298
|
+
const css = source.slice(start, end)
|
|
299
|
+
const offenders = [...css.matchAll(/([a-z-]*(?:color|background|border))\s*:\s*([^;"]+)/g)]
|
|
300
|
+
.map((m) => `${m[1]}:${m[2]}`)
|
|
301
|
+
.filter((decl) => {
|
|
302
|
+
const value = decl.slice(decl.indexOf(':') + 1)
|
|
303
|
+
if (value.includes('var(--dsw-alias-')) return false
|
|
304
|
+
// a literal colour is the problem; `transparent`/`none`/lengths are fine
|
|
305
|
+
return /#[0-9a-f]{3,8}|(?:rgba?|hsla?)\(|\b(?:white|black|gray|grey|silver)\b/i.test(value)
|
|
306
|
+
})
|
|
307
|
+
assert.deepEqual(offenders, [])
|
|
308
|
+
// the only literal left is the (theme-neutral) hairline card shadow
|
|
309
|
+
assert.match(css, /box-shadow:0 1px 2px rgba\(0,0,0,\.05\)/)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
test('client bundle manages workspace connections through configOp', () => {
|
|
313
|
+
assert.match(source, /configOp:\s*function/)
|
|
314
|
+
assert.match(source, /op: "add"/)
|
|
315
|
+
assert.match(source, /op: "remove"/)
|
|
316
|
+
assert.match(source, /op: "rename"/)
|
|
317
|
+
// the options-only write (read-only switch / SSH tunnel) never carries a DSN
|
|
318
|
+
assert.match(source, /op: "options"/)
|
|
319
|
+
assert.match(source, /_opKind: "rename"/)
|
|
320
|
+
assert.match(source, /_opKind: "options"/)
|
|
321
|
+
assert.match(source, /newEnv/)
|
|
322
|
+
assert.match(source, /renameFrom/)
|
|
323
|
+
assert.match(source, /workspacesOf\(value\)/)
|
|
324
|
+
assert.match(source, /已保存|自动/)
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
test('client bundle folds the add form behind an explicit entry point', () => {
|
|
328
|
+
assert.match(source, /var openAdd = function \(target\)/)
|
|
329
|
+
assert.match(source, /var closeAdd = function \(\)/)
|
|
330
|
+
assert.match(source, /t\("btn\.addConn"\)/)
|
|
331
|
+
assert.match(source, /onClick: function \(\) \{ openAdd\(g\.path\); \}/)
|
|
332
|
+
assert.match(source, /className: "dbh-addpanel"/)
|
|
333
|
+
// per-workspace adds carry the workspace implicitly
|
|
334
|
+
assert.match(source, /ws: addTarget \|\| addForm\.ws\.trim\(\)/)
|
|
335
|
+
assert.match(source, /id: "dbh-ws-list"/)
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
test('client bundle folds status/options into the header + a settings strip', () => {
|
|
339
|
+
assert.match(source, /className: "dbh-statustext", title: modeLabel/)
|
|
340
|
+
assert.match(source, /var toggleSettings = function \(\)/)
|
|
341
|
+
assert.match(source, /className: "dbh-strip"/)
|
|
342
|
+
assert.match(source, /t\("cfg\.summary", \{/)
|
|
343
|
+
assert.match(source, /className: "dbh-cfgsum"/)
|
|
344
|
+
// view state survives remounts within the page session
|
|
345
|
+
assert.match(source, /var uiMemory = \{ expanded: \{\}, addOpen: false, addTarget: "", settingsOpen: false, advOpen: false \};/)
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
test('client bundle offers a transient per-row connection test', () => {
|
|
349
|
+
assert.match(source, /op: "test"/)
|
|
350
|
+
assert.match(source, /nonce/)
|
|
351
|
+
assert.match(source, /value\.testResult/)
|
|
352
|
+
// results are surfaced only for nonces the card itself dispatched
|
|
353
|
+
assert.match(source, /nonceKeys/)
|
|
354
|
+
assert.match(source, /btn\.test/)
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
test('client bundle keeps the pre-test gate before saving a connection', () => {
|
|
358
|
+
assert.match(source, /var dispatchPretest = function \(form\)/)
|
|
359
|
+
assert.match(source, /"pretest:" \+ \(form\.ws \|\| ""\)/)
|
|
360
|
+
assert.match(source, /var confirmSave = function \(\)/)
|
|
361
|
+
assert.match(source, /t\("pretest\.failHint"\)/)
|
|
362
|
+
assert.match(source, /className: "dbh-pretest"/)
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test('client bundle injects only slots and probes the host transport', () => {
|
|
366
|
+
// Regression gate for the 0.2.x failure: a never-satisfied `settingsScope`
|
|
367
|
+
// injection left the plugin pending forever, so nothing rendered and nothing
|
|
368
|
+
// logged. Only `slots` may be injected; the face probes for the rest.
|
|
369
|
+
assert.match(source, /var inject = \["slots"\];/)
|
|
370
|
+
assert.doesNotMatch(source, /inject = \[[^\]]*"settingsScope"/)
|
|
371
|
+
// The live poll belongs to a rendered page, not to the plugin scope: an idle
|
|
372
|
+
// GUI must not poll the bridge.
|
|
373
|
+
assert.match(source, /props\.subscribe\(function \(\) \{ setValue\(valueOf\(props\.getSnapshot\(\)\)\); \}, \{ live: true \}\);/)
|
|
374
|
+
assert.match(source, /var live = !!\(opts && opts\.live\);/)
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
test('client bundle offers the environment read-only switch and never echoes the mirrored secret', () => {
|
|
378
|
+
assert.match(source, /"add\.readOnly": "只读模式"/)
|
|
379
|
+
assert.match(source, /"add\.readOnly": "Read-only mode"/)
|
|
380
|
+
assert.match(source, /var roCheck = function \(form, update, keyPrefix\)/)
|
|
381
|
+
// the checkbox is NOT a `.dbh-field`/`.dbh-input`: the add panel's field count
|
|
382
|
+
// and its 0..2 input indices are pinned by the offscreen gate
|
|
383
|
+
assert.match(source, /type: "checkbox", className: "dbh-checkbox"/)
|
|
384
|
+
assert.match(source, /"aria-label": t\("add\.readOnly"\)/)
|
|
385
|
+
// the very same draft builder feeds every op payload
|
|
386
|
+
assert.match(source, /function withConnDraft\(op, form\)/)
|
|
387
|
+
assert.match(source, /op\.readOnly = d\.readOnly === true;/)
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
test('client bundle configures the SSH tunnel, keeping secrets write-only', () => {
|
|
391
|
+
assert.match(source, /"add\.adv": "高级:SSH 隧道"/)
|
|
392
|
+
assert.match(source, /"add\.adv": "Advanced: SSH tunnel"/)
|
|
393
|
+
assert.match(source, /var advPanel = function \(form, update, keyPrefix, targetOf\)/)
|
|
394
|
+
// the SSH-layer-only test lives in the advanced section, with its own payload
|
|
395
|
+
assert.match(source, /"btn\.testSsh": "测试 SSH 隧道"/)
|
|
396
|
+
assert.match(source, /var startSshTest = function \(form, targetOf, key\)/)
|
|
397
|
+
assert.match(source, /op: "test", kind: "ssh",/)
|
|
398
|
+
assert.match(source, /"aria-expanded": open/)
|
|
399
|
+
// secrets are typed in and posted, never rendered back
|
|
400
|
+
assert.match(source, /"adv\.keepSecret"/)
|
|
401
|
+
assert.match(source, /type: "password", placeholder: t\("adv\.keepSecret"\)/)
|
|
402
|
+
// a blank secret means "keep the stored one": the builder only attaches the
|
|
403
|
+
// fields the user actually typed
|
|
404
|
+
assert.match(source, /if \(password\) ssh\.password/)
|
|
405
|
+
assert.match(source, /if \(keyPath\) ssh\.keyPath/)
|
|
406
|
+
assert.match(source, /if \(passphrase\) ssh\.passphrase/)
|
|
407
|
+
// the mirrored row only carries booleans for secrets
|
|
408
|
+
assert.match(source, /function sshDraftOf\(row\)/)
|
|
409
|
+
assert.match(source, /sshKeyPath: "",\s*\n\s*sshPassword: "",\s*\n\s*sshPassphrase: "",/)
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
test('client bundle shows the elapsed time of a running probe and cleans the ticker up', () => {
|
|
413
|
+
assert.match(source, /"test\.elapsed": "测试中… \{s\}s"/)
|
|
414
|
+
assert.match(source, /startedAt: Date\.now\(\)/)
|
|
415
|
+
assert.match(source, /t\("test\.elapsed", \{ s: String\(secs\) \}\)/)
|
|
416
|
+
// the 1s ticker exists ONLY while a row is testing, and its effect cleans up
|
|
417
|
+
// (the offscreen gate hangs on a leaked interval, which is the point)
|
|
418
|
+
assert.match(source, /if \(!anyTesting \|\| typeof setInterval !== "function"\) return undefined;/)
|
|
419
|
+
assert.match(source, /return function \(\) \{ clearInterval\(id\); \};/)
|
|
420
|
+
// probe watchdogs are tiered for the slower tunnel
|
|
421
|
+
assert.match(source, /var PRETEST_WATCHDOG_MS = 30000;/)
|
|
422
|
+
assert.match(source, /var PRETEST_WATCHDOG_SSH_MS = 60000;/)
|
|
423
|
+
assert.match(source, /var TEST_WATCHDOG_MS = 30000;/)
|
|
424
|
+
assert.match(source, /var TEST_WATCHDOG_SSH_MS = 60000;/)
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
test('client bundle disables what the installed dbhub cannot do', () => {
|
|
428
|
+
assert.match(source, /function capsOf\(value\)/)
|
|
429
|
+
assert.match(source, /function capabilityHint\(caps, t\)/)
|
|
430
|
+
assert.match(source, /"cap\.tooOld":/)
|
|
431
|
+
assert.match(source, /disabled: !sshSupported,/)
|
|
432
|
+
assert.match(source, /disabled: !roSupported,/)
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
test('client bundle gates every modern surface on the Host dsh version', () => {
|
|
436
|
+
// 5.2 surfaces (title/version header, update prompt, wide settings block,
|
|
437
|
+
// inline read-only toggle, switch-style controls) exist only on dsh >= 0.2.1.
|
|
438
|
+
// On 0.1.x the bundle must render the OLD DOM — the offscreen gate's legacy
|
|
439
|
+
// groups pin that, so the flag is read, never inferred from a slot existing.
|
|
440
|
+
assert.match(source, /var modern = value\.dshModern === true;/)
|
|
441
|
+
assert.match(source, /var pluginVersion = typeof value\.pluginVersion === "string"/)
|
|
442
|
+
assert.match(source, /var dshVersion = typeof value\.dshVersion === "string"/)
|
|
443
|
+
assert.match(source, /var runtime = value\.runtime === "desktop"/)
|
|
444
|
+
// The three mounts are named, not guessed: settings dialog vs full pages.
|
|
445
|
+
assert.match(source, /function SettingsSectionPanel\(props\)/)
|
|
446
|
+
assert.match(source, /function MainPanel\(props\)/)
|
|
447
|
+
assert.match(source, /surface: "modal"/)
|
|
448
|
+
assert.match(source, /surface: "main"/)
|
|
449
|
+
// Layout follows the measured column, with the phone check on the VIEWPORT
|
|
450
|
+
// (a narrow settings dialog on a desktop is not a phone).
|
|
451
|
+
assert.match(source, /function useMeasuredWidth\(ref\)/)
|
|
452
|
+
assert.match(source, /var isMobile = modern && \(viewportWidth > 0/)
|
|
453
|
+
assert.match(source, /var isWide = modern && !isMobile && surface !== "modal"/)
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
test('client bundle shows the plugin version and the quiet update prompt', () => {
|
|
457
|
+
assert.match(source, /"ver\.link": "可升级到 \{v\}"/)
|
|
458
|
+
assert.match(source, /"ver\.link": "Upgrade to \{v\}"/)
|
|
459
|
+
assert.match(source, /function parseUpdate\(value\)/)
|
|
460
|
+
// A text link by default; the instructions live in a panel opened on demand.
|
|
461
|
+
assert.match(source, /className: "dbh-updatelink"/)
|
|
462
|
+
assert.match(source, /className: "dbh-updatepanel"/)
|
|
463
|
+
assert.match(source, /t\("ver\.copySpec", \{ v: update\.latest \}\)/)
|
|
464
|
+
// the copy control copies the exact install spec; the web command is copiable too
|
|
465
|
+
assert.match(source, /dsh-dbhub-live@" \+ update\.latest/)
|
|
466
|
+
assert.match(source, /dsh plugin --profile web up dsh-dbhub-live@/)
|
|
467
|
+
// opening the page asks the Host for a throttled check; the button forces one
|
|
468
|
+
assert.match(source, /op: "updateCheck", force: false/)
|
|
469
|
+
assert.match(source, /op: "updateCheck", force: force === true/)
|
|
470
|
+
// Updating is neutral news, not an error: no red card/pill classes remain.
|
|
471
|
+
assert.doesNotMatch(source, /dbh-latest/)
|
|
472
|
+
assert.doesNotMatch(source, /dbh-updatecard/)
|
|
473
|
+
// desktop and web update routes differ
|
|
474
|
+
assert.match(source, /t\("ver\.desktopHint"/)
|
|
475
|
+
assert.match(source, /t\("ver\.webHint"/)
|
|
476
|
+
assert.match(source, /releases\/tag\/v/)
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
test('client bundle renders DSH-style settings rows, the harness Switch and row read-only chips', () => {
|
|
480
|
+
assert.match(source, /function switchControl\(opts\)/)
|
|
481
|
+
// The real toggle comes from the primitives module; the fallback is local.
|
|
482
|
+
assert.match(source, /primitives\.Switch/)
|
|
483
|
+
assert.match(source, /className: "dbh-fbswitch"/)
|
|
484
|
+
assert.match(source, /className: "dbh-settings"/)
|
|
485
|
+
assert.match(source, /className: "dbh-srow"/)
|
|
486
|
+
// Non-modal pages always use the horizontal bar (a narrow desktop column gets
|
|
487
|
+
// the compact variant instead of four stacked rows).
|
|
488
|
+
assert.match(source, /var useSettingsBar = modern && !isMobile && surface !== "modal"/)
|
|
489
|
+
assert.match(source, /className: "dbh-settings dbh-settingsbar"/)
|
|
490
|
+
// State words are two-character, current-state labels — never "已开启".
|
|
491
|
+
assert.match(source, /"row\.enable": "启用插件"/)
|
|
492
|
+
assert.match(source, /"state\.show": "显示"/)
|
|
493
|
+
assert.match(source, /"state\.hide": "隐藏"/)
|
|
494
|
+
assert.match(source, /"state\.on": "开启"/)
|
|
495
|
+
assert.match(source, /"state\.off": "关闭"/)
|
|
496
|
+
// Wide page owns the settings: enable / sidebar / policy switches + interval.
|
|
497
|
+
assert.match(source, /props\.setEnabled\(next\)/)
|
|
498
|
+
assert.match(source, /onChange: toggleSidebar/)
|
|
499
|
+
assert.match(source, /onChange: togglePolicy/)
|
|
500
|
+
// Read-only is a row chip in every layout, written as an options-only op.
|
|
501
|
+
assert.match(source, /className: "dbh-rochip"/)
|
|
502
|
+
assert.match(source, /op: "options", workspace: w\.path, env: w\.env, readOnly: w\.ro !== true/)
|
|
503
|
+
// The chip shows an OPEN lock + 可写 when writable and a CLOSED lock + 只读
|
|
504
|
+
// when locked, so a first-time user cannot mistake two writable rows for RO.
|
|
505
|
+
assert.match(source, /"dbh-roemoji"/)
|
|
506
|
+
assert.match(source, /on \? "🔒" : "🔓"/)
|
|
507
|
+
assert.match(source, /t\(on \? "ro\.chip" : "ro\.writable"\)/)
|
|
508
|
+
assert.match(source, /"ro\.writable": "可写"/)
|
|
509
|
+
assert.match(source, /"ro\.writable": "Writable"/)
|
|
510
|
+
// Long explanations are collapsed into the notes block.
|
|
511
|
+
assert.match(source, /className: "dbh-notes"/)
|
|
512
|
+
assert.match(source, /"note\.tools":/)
|
|
513
|
+
assert.match(source, /className: "dbh-titlebar"/)
|
|
514
|
+
assert.match(source, /className: "dbh-title"/)
|
|
515
|
+
})
|