dsh-plugin-showcase 0.1.0 → 0.2.0

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/README.md CHANGED
@@ -1,66 +1,87 @@
1
1
  # dsh-plugin-showcase
2
2
 
3
- A reference plugin for **DeepSeek Harness (DSH)**. It does one job — say one useful
4
- sentence about the DSH host it runs in — through all eight plugin surfaces at once,
5
- so the whole surface area can be read in one small package.
3
+ A starter companion for **DeepSeek Harness (DSH)**: it says one useful thing about the
4
+ host it runs in — and covers all eight plugin surfaces while doing it, so the whole
5
+ surface area can be read in one small package.
6
6
 
7
- | Surface | File and anchor |
7
+ Version **0.2.0**. The v1 surface is unchanged (`showcase_inspect`, `/showcase on|off`,
8
+ the gate, the three slots, locale, icon, patch); v2 adds a health check, two command
9
+ branches, a first-run guide in the panel, and a self-hosted market index.
10
+
11
+ | Surface | Anchor (v0.2.0) |
8
12
  | --- | --- |
9
- | Tool `showcase_inspect` | `lib/index.js:290` (definition), `:309` (registration) |
10
- | Hook `tools/pre-execute` | `lib/index.js:311` (gate), `:14`–`:31` (patterns) |
11
- | Slash command `/showcase` | `lib/index.js:322` |
12
- | Settings card | `lib/client.js:296` (`settings.section`, id `showcase`) |
13
- | Right-sidebar panel | body at `lib/client.js:314`, tab type via `sidebarRightTabs` |
14
- | i18n | `lib/client.js:12` (8 runtime keys), `locale/en.json`, `locale/zh.json` |
15
- | Marketplace metadata | `locale/*.json` meta, `icon.svg`, the `dsh` block in `package.json` |
13
+ | Tool `showcase_inspect` | `lib/index.js:600`, registered `:638` |
14
+ | Tool `showcase_doctor` | `lib/index.js:619`, registered `:639` |
15
+ | Hook `tools/pre-execute` | `lib/index.js:641` (gate), patterns `:21`–`:38` |
16
+ | Slash command `/showcase` | `lib/index.js:652` |
17
+ | Settings card | `lib/client.js:531` (`settings.section`, id `showcase`) |
18
+ | Right-sidebar panel | body `lib/client.js:551`, tab type `:539` |
19
+ | i18n | `lib/client.js:13` (19 keys), `locale/en.json`, `locale/zh.json` |
20
+ | Market index | `market/index.json` (shipped through `files`) |
16
21
  | Tests | `tests/host.test.mjs`, `tests/client.test.mjs` |
17
22
 
18
- Nothing here is compiled: only native ESM JavaScript, no third-party runtime
19
- dependency, no `@deepseek-ai` import.
23
+ Line numbers are as of v0.2.0. Nothing here is compiled: native ESM only, no
24
+ third-party runtime dependency, no `@deepseek-ai` import.
20
25
 
21
26
  ## Install and uninstall
22
27
 
23
- Profile used on this machine: `C:\Users\baba\.dsh\profiles\web`.
28
+ Profile on this machine: `C:\Users\baba\.dsh\profiles\web`.
29
+
30
+ ### A. From the market — the normal path
31
+
32
+ 1. Publish 0.2.0 to npm (see `PUBLISH.md`), so the index inside the package is
33
+ fetchable at
34
+ `https://cdn.jsdelivr.net/npm/dsh-plugin-showcase@latest/market/index.json`.
35
+ 2. Open DSH → Settings → the market / Discover page and add that URL as an index source.
36
+ The host service `pluginMarket` keeps that list (`indexes`), and it starts empty:
37
+ the Discover page does **not** search npm, so nothing shows up until an index is
38
+ configured.
39
+ 3. Search for `dsh-plugin-showcase` → install. The package lands in the profile and its
40
+ `dsh.bundle` entry is appended, so the layer activates.
24
41
 
25
- ### A. The documented way
42
+ The index ships inside the package, so it only exists from **0.2.0** onward: 0.1.0 was
43
+ published before `market/` existed and has no index. On any version >= 0.2.0 the CDN
44
+ URL above serves the index directly — no local server, no process to keep alive.
45
+
46
+ If you do not want to wait for the index, install by registry name instead:
26
47
 
27
48
  ```sh
28
- dsh plugin --profile web add <path-to-this-directory>
49
+ dsh plugin --profile web add dsh-plugin-showcase
29
50
  ```
30
51
 
31
- `dsh plugin` forwards its arguments to pnpm inside the profile, links the checkout,
32
- and — because the package declares `dsh.bundle` — appends it to
33
- `dsh.profile.bundles`.
52
+ ### B. From a checkout, with the documented command
34
53
 
35
- This host has no global `dsh` on `PATH`, so the same command through the checkout:
54
+ ```sh
55
+ dsh plugin --profile web add <path-to-this-directory>
56
+ ```
57
+
58
+ There is no global `dsh` on this machine's `PATH`, so the same command through the
59
+ checkout:
36
60
 
37
61
  ```sh
38
62
  node "C:\Users\baba\Desktop\deepseek-harness\apps\cli\lib\bin.js" plugin --profile web add "C:\Users\baba\Desktop\DSH工作区\plugins\dsh-plugin-showcase"
39
63
  ```
40
64
 
41
- Removal: `... plugin --profile web remove dsh-plugin-showcase` (drops the dependency
42
- and the bundle layer).
65
+ Removal: `... plugin --profile web remove dsh-plugin-showcase`.
43
66
 
44
- ### B. What this machine actually uses — the manual equivalent
67
+ ### C. What this machine actually uses — the manual equivalent
45
68
 
46
69
  Three edits, no install run:
47
70
 
48
71
  1. `C:\Users\baba\.dsh\profiles\web\package.json` → `dependencies`:
49
72
  `"dsh-plugin-showcase": "link:C:/Users/baba/Desktop/DSH工作区/plugins/dsh-plugin-showcase"`
50
- 2. The same file → `dsh.profile.bundles`: append `"dsh-plugin-showcase"`
51
- (it is currently the last entry).
52
- 3. A directory junction so Node can resolve the name:
73
+ 2. The same file → `dsh.profile.bundles`: append `"dsh-plugin-showcase"`.
74
+ 3. A directory junction so Node resolves the name:
53
75
  `C:\Users\baba\.dsh\profiles\web\node_modules\dsh-plugin-showcase`
54
76
  → `C:\Users\baba\Desktop\DSH工作区\plugins\dsh-plugin-showcase`
55
- (verified as a `<JUNCTION>` reparse point; recreate it with
56
- `cmd /c mklink /J <link> <target>`).
77
+ (verified `<JUNCTION>` reparse point; recreate with `cmd /c mklink /J <link> <target>`).
57
78
 
58
- **Why not the documented way here.** `dsh plugin ... add` runs an install inside the
59
- profile, and that install re-resolves the *whole* tree. This profile already carries
60
- eleven third-party plugins (`dsh-cost-meter`, `dsh-better-sidebar`, `dsh-chat-import`,
61
- `dsh-context`, `dsh-memgas`, `dsh-wallpaper-bg`, `@linxin666/*`, `@liustack/*`,
62
- `@tt-a1i/archify-dsh`). The three edits above are exactly the state a link install
63
- produces, without a resolver run that could rewrite one of those eleven.
79
+ **Why not the install command here.** It runs an install inside the profile, and that
80
+ re-resolves the *whole* tree. This profile already carries eleven third-party plugins
81
+ (`dsh-cost-meter`, `dsh-better-sidebar`, `dsh-chat-import`, `dsh-context`,
82
+ `dsh-memgas`, `dsh-wallpaper-bg`, `@linxin666/*`, `@liustack/*`, `@tt-a1i/archify-dsh`).
83
+ The three edits above are exactly the state a link install produces, without a resolver
84
+ run that could rewrite one of those eleven.
64
85
 
65
86
  Check the layer without starting the host (verified output):
66
87
 
@@ -72,99 +93,170 @@ node "C:\Users\baba\Desktop\deepseek-harness\apps\cli\lib\bin.js" --profile web
72
93
  # name: dsh-plugin-showcase
73
94
  ```
74
95
 
75
- DSH's HMR watches the profile `package.json` (the `bundles` array) and the profile
76
- patch files, so adding the entry recomposes the running host without a restart; the
77
- browser half needs one page refresh. Undo by removing the three edits.
96
+ DSH's HMR watches the profile `package.json` (the `bundles` array) and the profile patch
97
+ files, so appending the entry recomposes the running host without a restart; the browser
98
+ half needs one page refresh. Undo by removing the three edits.
99
+
100
+ ## The health check (new in 0.2.0)
101
+
102
+ `showcase_doctor` takes no arguments and returns a **HealthReport**:
103
+
104
+ ```
105
+ {
106
+ generatedAt: "ISO", pluginVersion: "0.2.0",
107
+ dshVersion: "0.2.1-rc.2", // string | null
108
+ summary: { ok, warn, fail, unknown }, // integers, always equal to the checks below
109
+ checks: [{ id, title, status, detail, hint }] // hint: string | null
110
+ }
111
+ ```
112
+
113
+ Two ways to ask:
114
+
115
+ - tool: run `showcase_doctor` (`showcase_inspect` still returns the v1 snapshot)
116
+ - command: `/showcase doctor` — the same text, straight into the conversation
117
+
118
+ The rendered text is `DSH 体检报告 <generatedAt>`, then one
119
+ `[OK|WARN|FAIL|?] <title> — <detail>` line per check, an indented ` ↳ <hint>` line when
120
+ there is a next step, and a closing `合计: ok N / warn N / fail N / unknown N`.
121
+
122
+ The six checks, in order:
123
+
124
+ | id | What it looks at | ok / warn / fail / unknown |
125
+ | --- | --- | --- |
126
+ | `engine.dsh` | running DSH version (read from `<profile>/node_modules/@deepseek-ai/dsh-app-boot/package.json`) against `engines.dsh` | ok = satisfies the range, fail = does not (hint: upgrade), warn = no `engines.dsh`, unknown = version unreadable or range not parseable offline |
127
+ | `workspace.cwd` | `workingDirectory` | ok = a directory, warn = none |
128
+ | `model.default` | `agentDefaultModel` | ok = `provider/model`, unknown = service missing |
129
+ | `plugin.gate` | this plugin's gate | ok = on, warn = off (hint: `/showcase on`) |
130
+ | `plugin.locale` | host language preference from `settings.describe()` | ok = a value, unknown = unreadable |
131
+ | `market.index` | index list from `pluginMarket.listIndexes()` | ok = every source readable, warn = none configured or some broken, unknown = no market service |
78
132
 
79
- ## Verifying each surface
133
+ **A check that throws never fails the run.** Its own row degrades to `unknown` with
134
+ `detail: "检查异常: <message>"` plus a hint; the other five keep their verdicts and the
135
+ summary still adds up to six.
80
136
 
81
- **1. Tool.** `showcase_inspect` takes no arguments and returns the snapshot. Ask the
82
- agent to run it; the rendered text starts with `DSH 巡检报告 <generatedAt>` and then
83
- `## 工作区`, `## 会话`, `## 插件`. It is also where the `null`/`""` degradation is
84
- visible: an unknown session shows `未知`.
137
+ ## Commands
85
138
 
86
- **2. Hook.** The gate is on by default. A tool call is denied when its `command`,
87
- `cmd`, `script` or `code` argument (case-insensitive) contains one of:
139
+ | Input | Answer |
140
+ | --- | --- |
141
+ | `/showcase` | the v1 inspection report text |
142
+ | `/showcase on` / `off` | `showcase 拦截门: 已开启` / `已关闭` |
143
+ | `/showcase doctor` | the health report text (same render as the tool) |
144
+ | `/showcase about` | one line: version, both tools, command branches, gate state, DSH version |
145
+ | anything else | `/showcase [on|off|doctor|about]` |
146
+
147
+ Input is trimmed and lower-cased first, so ` ON ` works and `Dr. Who` gets the usage line.
148
+
149
+ ## The guard gate
150
+
151
+ On by default. A tool call is denied when its `command`, `cmd`, `script` or `code`
152
+ argument (case-insensitive) contains one of:
88
153
 
89
154
  - `rm -rf /`, `rm -rf ~`
90
155
  - fork bombs: `:(){:|:&;}`, `:(){ :|:& };:`, `.(){ :|:& };:`, `bomb(){ bomb|bomb& };bomb`
91
156
  - `del /f /s /q c:\`, `format c:`
92
- - the `shutdown` / `shutdown.exe` word in command position, optionally behind
93
- `sudo`, `doas` or `pkexec`, e.g. `shutdown /s /t 0` or `sudo shutdown -h now`
94
-
95
- Whitespace-collapsed spellings such as `rm -rf /tmp`, or the same commands written with tabs
96
- instead of spaces, are caught too.
97
- The deny reason is `showcase: 拒绝执行破坏性命令`; everything else calls `next()` and
98
- is allowed. Argument text outside those four keys is ignored.
99
-
100
- **3. Command.** `/showcase` prints the same report text; `/showcase on` and
101
- `/showcase off` switch the gate (`showcase 拦截门: 已开启` / `已关闭`); any other input
102
- answers `/showcase [on|off]`. State is per plugin instance, so a fresh apply starts
103
- with the gate on.
104
-
105
- **4. Settings card.** Open Settings: a nav row labelled **功能展示 / Showcase**
106
- (registration id `showcase`, order 90) holds the card — a refresh button, an
107
- "open the panel" button, and then either the snapshot facts or the degraded copy.
108
-
109
- **5. Right-sidebar panel.** Tab type id `showcase-panel`, kind `showcase`, title
110
- **巡检面板 / Showcase panel**, body seated under the same key. Two doors open it: the ◎
111
- button in the left sidebar footer (beside Settings), and the settings card's
112
- "open the panel" button — both end in `sidebarRight.openTab('showcase')`. The type
113
- declares no `guide` entry, so it does not appear on the right sidebar's guide page.
114
-
115
- **6. i18n.** Eight runtime keys — `title`, `subtitle`, `panelTitle`, `panelEmpty`,
116
- `gateOn`, `gateOff`, `openPanel`, `refresh` — each present in `en` and `zh`.
157
+ - the `shutdown` / `shutdown.exe` word in command position, optionally behind `sudo`,
158
+ `doas` or `pkexec`, e.g. `shutdown /s /t 0` or `sudo shutdown -h now`
159
+
160
+ Whitespace-collapsed spellings such as `rm -rf /tmp`, or the same commands written with
161
+ tabs instead of spaces, are caught too. The deny reason is
162
+ `showcase: 拒绝执行破坏性命令`; everything else calls `next()` and is allowed. Text
163
+ outside those four argument keys is ignored, and a fault inside the hook still calls
164
+ `next()` — a plugin bug must never block tool calls.
165
+
166
+ ## Settings card and panel
167
+
168
+ **Settings card** — Settings → nav row **功能展示 / Showcase** (id `showcase`, order 90):
169
+ title and subtitle, the doctor hint, a copyable `/showcase doctor` row, refresh and
170
+ "open the panel" buttons, a footer with `v0.2.0` and the gate state, the three capability
171
+ rows, then either the snapshot or the degraded copy.
172
+
173
+ **Right-sidebar panel** — tab type id `showcase-panel`, kind `showcase`, title
174
+ **巡检面板 / Showcase panel**, body seated under the same key. Open it from the ◎ button
175
+ in the left sidebar footer (beside Settings) or from the settings card's button; both end
176
+ in `sidebarRight.openTab('showcase')`. The type declares no `guide` entry, so it does not
177
+ appear on the right sidebar's guide page.
178
+
179
+ The panel opens on a three-step guide: 1) run a health check — the `/showcase doctor`
180
+ text is shown and copyable, 2) open the settings card, 3) understand the gate —
181
+ `/showcase on|off` is shown and copyable. Each step has its own copy button; pressing it
182
+ flips the label to **已复制 / Copied** for about 1.6 s. The footer carries `v0.2.0` and,
183
+ while no snapshot is reachable, the degraded line.
184
+
185
+ ## i18n
186
+
187
+ 19 runtime keys, each present in `en` and `zh`, translated rather than copied:
188
+
189
+ `title` `subtitle` `panelTitle` `panelEmpty` `gateOn` `gateOff` `openPanel` `refresh`
190
+ (the v1 eight) plus `welcome` `stepDoctorTitle` `stepDoctorBody` `stepSettingsTitle`
191
+ `stepSettingsBody` `stepGateTitle` `stepGateBody` `copyHint` `copied` `doctorHint`
192
+ `aboutBody`.
193
+
117
194
  Language resolution is `ctx.locale` → `navigator.language` → `en`; a host without a
118
- locale service still renders real copy. Marketplace metadata comes from
119
- `locale/en.json` (the anchor) and `locale/zh.json`.
195
+ locale service still renders real copy.
196
+
197
+ ## Market index (self-hosted inside the package)
198
+
199
+ Since v0.2.0 the index travels with the package: `market/index.json` (format 1, listing
200
+ this plugin) is whitelisted in `package.json` → `files`, so it is inside the published
201
+ tarball. Public address:
202
+
203
+ ```
204
+ https://cdn.jsdelivr.net/npm/dsh-plugin-showcase@latest/market/index.json
205
+ ```
206
+
207
+ Add that URL as an index source on the Discover page and the plugin is installable
208
+ without any local server.
120
209
 
121
- **7. Marketplace metadata.** The Plugins page shows `icon.svg` plus the localized
122
- title and description from the `locale/*.json` `meta` blocks.
210
+ The temporary service this machine used during development still listens on
211
+ `http://127.0.0.1:8791` (its index answers 200 at `/index.json`). It is **no longer
212
+ needed**: pointing the Discover page at the URL above replaces it, and after that the
213
+ local service can be stopped.
123
214
 
124
- **8. Tests.** `npm test` — see below.
215
+ Marketplace metadata still comes from `locale/en.json` (the anchor) and `locale/zh.json`,
216
+ so the Plugins page shows the localized title and description next to `icon.svg`.
125
217
 
126
218
  ## Honest limitations
127
219
 
128
- 1. **The client cannot read live host data, and under the zero-build rule it cannot.**
129
- The documented remote-API path needs typert decorators, a code-generation step and
130
- a third-party dependency — three hard conflicts with "native ESM, no build, no
131
- deps". `lib/client.js` still probes the plausible shapes (`remote.showcase`,
132
- `ctx.showcase`) and degrades to `null`. Consequence: the panel and the settings
133
- card show a **static capability list plus the degraded copy**, not host data.
134
- 2. **`sidebar.footer.action` does not project `label`.** The sidebar shell renders
135
- that seat with only `{ wide }`, so the visible text is whatever the component
136
- draws (`◎`, with the label carried as `aria-label`). The `label` thunk is a legal
137
- `SlotLabel` and is correct, but no shell reads it for this seat.
138
- 3. **The gate is a literal-substring heuristic, not a sandbox.** It normalizes case and
139
- whitespace and understands command position, but an obfuscated spelling (base64,
140
- variable expansion, another tool's argument key) passes through. It is here to
141
- demonstrate the extension point and to stop obvious mistakes, nothing more.
142
- 4. **At 390px the settings page is limited by the host layout.** The host's settings
143
- dialog does not collapse its left navigation at that width, so the content column is
144
- very narrow. This card stacks its key/value pairs when the container is under 320px,
145
- lets long values wrap anywhere and hides horizontal overflow, but the page margins
146
- belong to the host.
147
- 5. **The visual regression covers facts, not pixels.** A real browser (Edge driven by
148
- `playwright-core`, no browser download) captured light, dark and 390px states,
149
- asserting that elements render, that nothing overflows horizontally, and the measured
150
- theme colours. Screenshots and the script live in `_visual/`; there is no per-pixel
151
- baseline diff.
220
+ 1. **The client still cannot read live host data, and under the zero-build rule it
221
+ cannot.** The documented remote-API path needs typert decorators, a code-generation
222
+ step and a third-party dependency — three hard conflicts with "native ESM, no build,
223
+ no deps". `lib/client.js` probes the plausible shapes (`remote.showcase`,
224
+ `ctx.showcase`) and degrades to `null`. So the card and the panel show the static
225
+ capability rows and the degraded copy, never a live snapshot; that is also why the
226
+ first-run guide hands you the `/showcase doctor` text instead of calling the host.
227
+ 2. **`sidebar.footer.action` does not project `label`.** The sidebar shell renders that
228
+ seat with only `{ wide }`, so the visible text is what the component draws (`◎`, with
229
+ the label carried as `aria-label`). The `label` thunk is a legal `SlotLabel` and is
230
+ correct, but no shell reads it for this seat.
231
+ 3. **Around 390 px the layout is the host's call, not ours.** The seats stack when the
232
+ *measured box* drops below 320 px, and every box is `min-width: 0` plus
233
+ `overflow-x: hidden`, so a squeezed container can only shorten lines. But the right
234
+ column's width is clamped by DSH itself: the plugin cannot make the pane narrower or
235
+ wider than the host allows, and behaviour below roughly 390 px was only reached
236
+ through container measurement, not through a real narrow viewport.
237
+ 4. **No visual regression in a real browser — the tests cover facts, not pixels.** The
238
+ client evidence comes from Node stub tests: `react` is replaced by a minimal
239
+ `createElement`/`useState`/`useEffect`/`useRef` stand-in, so the assertions are the
240
+ element tree and its text (19 bilingual keys, three step titles, the copied label after
241
+ a click, the degraded copy). There is no screenshot baseline and no pixel comparison.
152
242
 
153
243
  ## Extending it
154
244
 
155
245
  - **New tool.** Copy the raw JSON-Schema `ToolDefinition` shape of `inspectTool`
156
- (`lib/index.js:290`) and register it with
157
- `ctx.effect(() => ctx.tools.register(<tool>), 'showcase: <name>')` (`:309`). `output`
158
- must carry `{ schema, render }`. Add its deny patterns to `DESTRUCTIVE_PATTERNS`
159
- (`:14`), then cover it in `tests/host.test.mjs`.
160
- - **New slot.** In `apply()` (`lib/client.js:290`) copy the
161
- `slots.inject(<slot>, () => slots.register({ name, ... }, Component))` shape:
162
- `settings.section` at `:296`, `sidebar.right.pane.tab` at `:314`,
163
- `sidebar.footer.action` at `:322`. A right-sidebar tab needs both halves — the body
164
- key must equal the id of the type registered through `sidebarRightTabs.register`.
246
+ (`lib/index.js:600`) or `doctorTool` (`:619`) and register it with
247
+ `ctx.effect(() => ctx.tools.register(<tool>), 'showcase: <name>')` (`:638` / `:639`).
248
+ `output` must carry `{ schema, render }`.
249
+ - **New check.** Add one `runCheck('<id>', '<title>', () => …)` entry to the array in
250
+ `buildHealthReport` (`lib/index.js:511`), keep it wrapped so a throw becomes
251
+ `unknown`, and extend `buildHealthSchema` only if the shape really changes.
252
+ - **New slot.** In `apply()` (`lib/client.js:525`) copy the
253
+ `slots.inject(<slot>, () => slots.register({ name, … }, Component))` shape:
254
+ `settings.section` at `:531`, `sidebar.right.pane.tab` at `:551`,
255
+ `sidebar.footer.action` at `:559`. A right-sidebar tab needs both halves — the body key
256
+ must equal the id of the type registered through `sidebarRightTabs`.
165
257
  - **New runtime string.** Add the key to both `dict.en` and `dict.zh`
166
- (`lib/client.js:12`) and extend `DICT_KEYS` in
167
- `tests/helpers/client-harness.mjs` so the bilingual test keeps guarding it.
258
+ (`lib/client.js:13`) and extend `DICT_KEYS` in `tests/helpers/client-harness.mjs`, so
259
+ the bilingual test keeps guarding it.
168
260
 
169
261
  ## Tests
170
262
 
@@ -173,20 +265,23 @@ npm install # once, needs registry access (devDependency: vitest)
173
265
  npm test # offline from then on, one command, whole suite
174
266
  ```
175
267
 
176
- 72 tests in 2 files, no machine path baked in — every path derives from
177
- `import.meta.url`, and the workspace fixture is a temp directory created per run.
178
-
179
- - `tests/host.test.mjs` (47): module shape, `ctx.effect` lifecycle, tool registration,
180
- `output` contract with the returned value validated against its own schema, snapshot
181
- shape, "no `undefined`", `""`/`null` degradation, git-branch discovery, all eight
182
- destructive patterns (plus collapsed spellings, array arguments, key scoping), the
183
- word-boundary deny and allow cases for power commands, hook-throw still allowing,
184
- gate on/off, the command's three inputs, the provided service.
185
- - `tests/client.test.mjs` (25): lazy-CJS registration, factory exports, exactly three
186
- seats, tab-type/body-key agreement, the footer's open path, the eight bilingual dict
187
- keys, language resolution including a host with no locale service, `fetchSnapshot`
188
- degradation (no service / `ok` / bare snapshot / `ok:false` / throwing / malformed),
189
- rendering with and without data, refresh clicks.
190
-
191
- Runner: **vitest 3** (the declared devDependency). The fallback would have been Node's
192
- built-in `node:test`; it was not needed because the registry was reachable.
268
+ 98 tests in 2 files, no machine path baked in — every path derives from
269
+ `import.meta.url`, and the fixtures (a workspace, a two-version DSH profile) are temp
270
+ directories created per run.
271
+
272
+ - `tests/host.test.mjs` (66): module shape, the `ctx.effect` lifecycle of all four
273
+ registrations, both tools' parameters and `output` contract with returned values
274
+ validated against their own schemas, snapshot shape and `""`/`null` degradation,
275
+ git-branch discovery, all eight destructive patterns plus collapsed spellings and key
276
+ scoping, the word-boundary cases for power commands, hook-throw still allowing, gate
277
+ on/off, the HealthReport shape with "no `undefined`" and summary-vs-checks agreement,
278
+ each of the six checks in its ok / warn / unknown / fail branches, a throwing check
279
+ degrading on its own, the report render layout, and the five command inputs.
280
+ - `tests/client.test.mjs` (32): lazy-CJS registration, factory exports including
281
+ `VERSION`, exactly three seats, tab-type/body-key agreement and late service arrival,
282
+ the footer's open path, all 19 bilingual dict keys, the three step titles, language
283
+ resolution including a host with no locale service, `fetchSnapshot` degradation,
284
+ rendering with and without data, the first-run guide, the copy-to-"已复制" flip, and
285
+ refresh clicks.
286
+
287
+ Runner: **vitest 3** (the declared devDependency).