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.
Files changed (50) hide show
  1. package/AGENTS.md +95 -209
  2. package/README.en.md +251 -169
  3. package/README.md +251 -169
  4. package/doc/ARCHITECTURE.md +108 -0
  5. package/doc/CLIENT.md +124 -0
  6. package/doc/DESENSITIZE.md +56 -0
  7. package/doc/DEVELOPMENT.md +139 -0
  8. package/doc/IMPACT-MATRIX.md +36 -0
  9. package/doc/RELEASE.md +93 -0
  10. package/doc/REQUIREMENTS.md +177 -105
  11. package/doc/images/chat-query.png +0 -0
  12. package/doc/images/row-editor.png +0 -0
  13. package/doc/images/settings-page.png +0 -0
  14. package/lib/adhoc.mjs +412 -34
  15. package/lib/capability.mjs +157 -0
  16. package/lib/client.js +1243 -80
  17. package/lib/config.mjs +370 -21
  18. package/lib/i18n.mjs +119 -4
  19. package/lib/index.mjs +401 -43
  20. package/lib/mcp.mjs +496 -391
  21. package/lib/meta.mjs +267 -0
  22. package/lib/options.mjs +25 -5
  23. package/lib/policy.mjs +92 -0
  24. package/lib/toml.mjs +188 -0
  25. package/lib/tools.mjs +1093 -827
  26. package/lib/update.mjs +244 -0
  27. package/package.json +34 -10
  28. package/scripts/desensitize-check.mjs +10 -3
  29. package/scripts/desensitize.config.mjs +255 -237
  30. package/scripts/docs-check.mjs +214 -0
  31. package/scripts/release-preflight.mjs +180 -0
  32. package/scripts/render-check.mjs +1366 -843
  33. package/test/adhoc-toml.test.mjs +365 -0
  34. package/test/bridge.test.mjs +227 -185
  35. package/test/capability.test.mjs +231 -0
  36. package/test/client-format.test.mjs +515 -352
  37. package/test/configure-options.test.mjs +244 -0
  38. package/test/configure-ssh.test.mjs +232 -0
  39. package/test/copy-from.test.mjs +155 -0
  40. package/test/env-options.test.mjs +456 -0
  41. package/test/meta.test.mjs +234 -0
  42. package/test/options.test.mjs +160 -123
  43. package/test/policy.test.mjs +132 -0
  44. package/test/row-cache.test.mjs +237 -138
  45. package/test/ssh-probe.test.mjs +834 -0
  46. package/test/test-op-fastpath.test.mjs +208 -0
  47. package/test/toml.test.mjs +352 -0
  48. package/test/update.test.mjs +270 -0
  49. package/test/util.test.mjs +17 -0
  50. package/test/zero-knowledge.test.mjs +120 -78
@@ -0,0 +1,231 @@
1
+ // Unit tests for lib/capability.mjs — the dbhub version / capability guard.
2
+ //
3
+ // capability.mjs is a PURE module (node:fs + node:path only, it does not import
4
+ // config.mjs), so unlike the storage-bound lib tests it needs no DSH_HOME.
5
+ //
6
+ // Fixtures obey the desensitization convention: loopback / RFC 5737 hosts and
7
+ // placeholder secrets only. The dbhub executable itself is never created — the
8
+ // detector is deliberately file-based and only reads package.json manifests.
9
+
10
+ import test from 'node:test'
11
+ import assert from 'node:assert/strict'
12
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
13
+ import { tmpdir } from 'node:os'
14
+ import { join } from 'node:path'
15
+
16
+ const cap = await import('../lib/capability.mjs')
17
+
18
+ const tempDirs = []
19
+
20
+ function tempDir(tag) {
21
+ const dir = mkdtempSync(join(tmpdir(), 'dsh-dbhub-cap-' + tag + '-'))
22
+ tempDirs.push(dir)
23
+ return dir
24
+ }
25
+
26
+ /**
27
+ * The layout runtime.mjs produces with
28
+ * `npm install @bytebase/dbhub --prefix <RUNTIME_DIR>`:
29
+ * <dir>/node_modules/.bin/dbhub.cmd
30
+ * <dir>/node_modules/@bytebase/dbhub/package.json
31
+ * The shim file itself need not exist (and is never read).
32
+ */
33
+ function managedInstall(version) {
34
+ const dir = tempDir('managed')
35
+ const pkgDir = join(dir, 'node_modules', '@bytebase', 'dbhub')
36
+ mkdirSync(pkgDir, { recursive: true })
37
+ writeFileSync(join(pkgDir, 'package.json'), JSON.stringify({ name: '@bytebase/dbhub', version }))
38
+ return { dir, exe: join(dir, 'node_modules', '.bin', 'dbhub.cmd') }
39
+ }
40
+
41
+ /** The mise-install path form; the shim file need not exist either. */
42
+ function miseExe(version) {
43
+ return join(tempDir('mise'), 'npm-bytebase-dbhub', version, 'node_modules', '.bin', 'dbhub.cmd')
44
+ }
45
+
46
+ test.after(() => {
47
+ for (const dir of tempDirs) rmSync(dir, { recursive: true, force: true })
48
+ })
49
+
50
+ // ── pure version helpers ──────────────────────────────────────────────────
51
+
52
+ test('parseVersion accepts a leading v, prerelease/build suffixes and a missing patch', () => {
53
+ assert.deepEqual(cap.parseVersion('1.2.3'), { major: 1, minor: 2, patch: 3 })
54
+ assert.deepEqual(cap.parseVersion('v1.2.3'), { major: 1, minor: 2, patch: 3 })
55
+ assert.deepEqual(cap.parseVersion('1.4.0-beta.1'), { major: 1, minor: 4, patch: 0 })
56
+ assert.deepEqual(cap.parseVersion('1.4.0+build.9'), { major: 1, minor: 4, patch: 0 })
57
+ assert.deepEqual(cap.parseVersion('1.2'), { major: 1, minor: 2, patch: 0 })
58
+ assert.deepEqual(cap.parseVersion(' v0.9.0 '), { major: 0, minor: 9, patch: 0 })
59
+ // only a LEADING numeric triplet counts — a version buried in text is not one
60
+ for (const bad of ['', 'garbage', 'dbhub 1.4.0', 'v', 'x1.2.3', '1', '1.x', undefined, null, 42, {}]) {
61
+ assert.equal(cap.parseVersion(bad), null, 'expected null for ' + String(bad))
62
+ }
63
+ })
64
+
65
+ test('compareVersions orders by major/minor/patch and atLeast is inclusive', () => {
66
+ const p = cap.parseVersion
67
+ assert.equal(cap.compareVersions(p('1.2.3'), p('1.2.3')), 0)
68
+ assert.ok(cap.compareVersions(p('1.2.3'), p('1.2.4')) < 0)
69
+ assert.ok(cap.compareVersions(p('1.3.0'), p('1.2.9')) > 0)
70
+ assert.ok(cap.compareVersions(p('2.0.0'), p('1.9.9')) > 0)
71
+ // a missing side counts as 0.0.0 (defensive, never throws)
72
+ assert.equal(cap.compareVersions(null, null), 0)
73
+ assert.equal(cap.compareVersions(p('0.0.1'), null), 1)
74
+ assert.equal(cap.compareVersions(null, p('0.0.1')), -1)
75
+ // atLeast: inclusive floor, prerelease suffix ignored, unparsable -> false
76
+ assert.equal(cap.atLeast(p('1.4.0'), '1.4.0'), true)
77
+ assert.equal(cap.atLeast(p('1.4.1'), '1.4.0'), true)
78
+ assert.equal(cap.atLeast(p('1.4.0-beta.1'), '1.4.0'), true)
79
+ assert.equal(cap.atLeast(p('1.3.9'), '1.4.0'), false)
80
+ assert.equal(cap.atLeast(p('0.9.0'), '1.0.0'), false)
81
+ assert.equal(cap.atLeast(null, '1.4.0'), false)
82
+ assert.equal(cap.atLeast(p('1.4.0'), 'not-a-version'), false)
83
+ // the documented floors
84
+ assert.equal(cap.DBHUB_MIN_VERSION, '1.4.0')
85
+ assert.equal(cap.DBHUB_TOML_SINCE, '1.0.0')
86
+ })
87
+
88
+ test('capabilitiesOf maps the version matrix onto the two generated features', () => {
89
+ // < 1.0.0 predates the TOML tool model entirely -> refuse both features
90
+ assert.deepEqual(cap.capabilitiesOf('0.9.0'), {
91
+ version: '0.9.0', readonlyTools: false, ssh: false, warning: 'tooOld',
92
+ })
93
+ assert.equal(cap.capabilitiesOf('0.0.1').warning, 'tooOld')
94
+ // 1.0.0-1.3.x: allowed but never measured -> warn
95
+ assert.deepEqual(cap.capabilitiesOf('1.2.0'), {
96
+ version: '1.2.0', readonlyTools: true, ssh: true, warning: 'untested',
97
+ })
98
+ assert.equal(cap.capabilitiesOf('1.0.0').warning, 'untested')
99
+ assert.equal(cap.capabilitiesOf('1.3.99').warning, 'untested')
100
+ // >= 1.4.0: the dialect this plugin was measured against -> no warning
101
+ assert.deepEqual(cap.capabilitiesOf('1.4.0'), {
102
+ version: '1.4.0', readonlyTools: true, ssh: true, warning: '',
103
+ })
104
+ assert.equal(cap.capabilitiesOf('1.5.2').warning, '')
105
+ assert.equal(cap.capabilitiesOf('v2.0.0').warning, '')
106
+ })
107
+
108
+ test('capabilitiesOf reports unknown (features ON) for an unparsable version', () => {
109
+ for (const raw of ['', 'garbage', undefined, null]) {
110
+ const c = cap.capabilitiesOf(raw)
111
+ assert.equal(c.readonlyTools, true)
112
+ assert.equal(c.ssh, true)
113
+ assert.equal(c.warning, 'unknown')
114
+ }
115
+ // the raw value is echoed back as a string so the UI can show what was seen
116
+ assert.equal(cap.capabilitiesOf('').version, '')
117
+ assert.equal(cap.capabilitiesOf(undefined).version, '')
118
+ assert.equal(cap.capabilitiesOf('garbage').version, 'garbage')
119
+ })
120
+
121
+ // ── executable-path detection ─────────────────────────────────────────────
122
+
123
+ test('a managed install resolves the version from its package.json', () => {
124
+ const { exe } = managedInstall('1.4.0')
125
+ const detected = cap.detectVersion(exe)
126
+ const c = cap.detectDbhubCapabilities(exe)
127
+ // KNOWN GAP, reported rather than worked around: versionFromPackageJson only
128
+ // probes `<dir>/package.json` and `<dir>/node_modules/dbhub/package.json`, so
129
+ // the SCOPED manifest of this plugin's own managed install
130
+ // (`<dir>/node_modules/@bytebase/dbhub/package.json`, see runtime.mjs) is
131
+ // never read. Fix: probe `join(dir, 'node_modules', '@bytebase', 'dbhub',
132
+ // 'package.json')` as well.
133
+ assert.equal(
134
+ detected,
135
+ '1.4.0',
136
+ 'managed install not detected (see the KNOWN GAP note above)',
137
+ )
138
+ assert.equal(c.version, '1.4.0')
139
+ assert.equal(c.readonlyTools, true)
140
+ assert.equal(c.ssh, true)
141
+ assert.equal(c.warning, '')
142
+ })
143
+
144
+ test('a mise layout resolves the version from the path segment', () => {
145
+ const exe = miseExe('1.2.0')
146
+ const c = cap.detectDbhubCapabilities(exe)
147
+ assert.equal(c.version, '1.2.0')
148
+ assert.equal(c.readonlyTools, true)
149
+ assert.equal(c.ssh, true)
150
+ assert.equal(c.warning, 'untested')
151
+ assert.equal(cap.detectVersion(miseExe('1.4.0')), '1.4.0')
152
+ assert.equal(cap.detectDbhubCapabilities(miseExe('1.4.0')).warning, '')
153
+ // a non-version segment is not a version
154
+ assert.equal(cap.versionFromMisePath(miseExe('latest')), '')
155
+ assert.equal(cap.versionFromMisePath('/usr/local/bin/dbhub'), '')
156
+ })
157
+
158
+ test('a pre-1.0 install refuses BOTH features with an upgrade hint', () => {
159
+ const c = cap.detectDbhubCapabilities(miseExe('0.9.0'))
160
+ assert.equal(c.version, '0.9.0')
161
+ assert.equal(c.readonlyTools, false)
162
+ assert.equal(c.ssh, false)
163
+ assert.equal(c.warning, 'tooOld')
164
+ })
165
+
166
+ test('an unresolvable executable reports unknown and never throws', () => {
167
+ // Deep enough that the bounded walk-up stays inside the temp dir, and with no
168
+ // package.json and no mise segment anywhere above it.
169
+ const deep = join(tempDir('unknown'), 'a', 'b', 'c', 'd', 'e', 'node_modules', '.bin')
170
+ mkdirSync(deep, { recursive: true })
171
+ const exe = join(deep, 'dbhub.cmd')
172
+ assert.equal(cap.detectVersion(exe), '')
173
+ const c = cap.detectDbhubCapabilities(exe)
174
+ assert.equal(c.version, '')
175
+ assert.equal(c.readonlyTools, true)
176
+ assert.equal(c.ssh, true)
177
+ assert.equal(c.warning, 'unknown')
178
+ // the defensive surface: unresolvable PATHS must not throw either
179
+ for (const bad of [join(deep, 'nope.cmd'), join(deep, 'gone', 'dbhub.cmd'), exe.toUpperCase()]) {
180
+ const safe = cap.detectDbhubCapabilities(bad)
181
+ assert.equal(safe.readonlyTools, true)
182
+ assert.equal(safe.ssh, true)
183
+ assert.equal(safe.warning, 'unknown')
184
+ }
185
+ // REPORTED (not asserted here, no spec for it): a bare relative name is NOT
186
+ // an unresolvable path for this detector. `detectDbhubCapabilities('garbage')`
187
+ // resolves it against process.cwd() and then accepts any manifest whose name
188
+ // merely matches /dbhub/i — including this plugin's OWN package.json
189
+ // (`dsh-dbhub-live`), whose version it then reports with warning ''.
190
+ // Measured here: detectDbhubCapabilities('garbage') -> the repository's own
191
+ // package.json version, warning ''. The pure `capabilitiesOf('garbage')` is
192
+ // unaffected (asserted above with the empty/unparsable matrix).
193
+ })
194
+
195
+ test('versionFromPackageJson only trusts a dbhub-looking manifest', () => {
196
+ // Nested deep enough that the bounded (6-level) walk-up stays inside the temp
197
+ // dir whatever lives above the OS temp root.
198
+ const base = join(tempDir('foreign'), 'a', 'b', 'c', 'd', 'e', 'f')
199
+ const bin = join(base, 'node_modules', '.bin')
200
+ mkdirSync(bin, { recursive: true })
201
+ // a foreign package.json directly above the shim: its version is NOT dbhub's
202
+ writeFileSync(join(base, 'package.json'), JSON.stringify({ name: 'some-other-tool', version: '9.9.9' }))
203
+ assert.equal(cap.versionFromPackageJson(join(bin, 'dbhub.cmd')), '')
204
+ // the unscoped `node_modules/dbhub` form IS trusted when it is really dbhub
205
+ const unscoped = tempDir('unscoped')
206
+ const inner = join(unscoped, 'node_modules', 'dbhub')
207
+ mkdirSync(inner, { recursive: true })
208
+ writeFileSync(join(inner, 'package.json'), JSON.stringify({ name: 'dbhub', version: '1.4.0' }))
209
+ assert.equal(cap.versionFromPackageJson(join(unscoped, 'node_modules', '.bin', 'dbhub.cmd')), '1.4.0')
210
+ })
211
+
212
+ // ── manifest trust ────────────────────────────────────────────────────────
213
+
214
+ test('an unrelated manifest above the executable never lends its version', () => {
215
+ // Regression: the resolver walks UP from the executable, so without a name
216
+ // check it could read any package.json above it — including this plugin's own
217
+ // (`dsh-dbhub-live`, whose name merely CONTAINS "dbhub") when the executable
218
+ // path is relative or empty.
219
+ const dir = tempDir('foreign-manifest')
220
+ writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'dsh-dbhub-live', version: '9.9.9' }))
221
+ const fakeExe = join(dir, 'node_modules', '.bin', 'dbhub.cmd')
222
+ assert.equal(cap.versionFromPackageJson(fakeExe), '', 'a foreign name is not a dbhub version')
223
+ const detected = cap.detectDbhubCapabilities(fakeExe)
224
+ assert.equal(detected.version, '')
225
+ assert.equal(detected.warning, 'unknown')
226
+ assert.equal(detected.readonlyTools, true, 'an unknown version must not disable a working setup')
227
+ // An empty/blank executable can never be resolved into somebody's manifest.
228
+ assert.equal(cap.detectVersion(''), '')
229
+ assert.equal(cap.detectVersion(' '), '')
230
+ assert.equal(cap.detectDbhubCapabilities('').warning, 'unknown')
231
+ })