dsh-background-by-model 0.4.3 → 0.4.5

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.
@@ -219,7 +219,8 @@ const HOST_CONTRACTS = [
219
219
  checks: [{
220
220
  kind: "selector",
221
221
  selector: "div[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]",
222
- optionalWhen: "settingsClosed"
222
+ optionalWhen: "settingsOpen",
223
+ visible: true
223
224
  }],
224
225
  sources: [{
225
226
  path: "dsh-client-ui-primitives/lib/index.js",
@@ -275,7 +276,7 @@ const HOST_CONTRACTS = [
275
276
  checks: [{
276
277
  kind: "attr",
277
278
  attr: "data-composer-card",
278
- optionalWhen: "chatNotMounted"
279
+ optionalWhen: "chatHasMessages"
279
280
  }],
280
281
  sources: [{
281
282
  path: "dsh-client-ui-conversation/lib/client.js",
@@ -301,7 +302,7 @@ const HOST_CONTRACTS = [
301
302
  checks: [{
302
303
  kind: "attr",
303
304
  attr: "data-cordis-panel",
304
- optionalWhen: "cordisClosed"
305
+ optionalWhen: "cordisMounted"
305
306
  }],
306
307
  sources: [{
307
308
  path: "dsh-client-ui-cordis/lib/client.js",
@@ -327,7 +328,8 @@ const HOST_CONTRACTS = [
327
328
  checks: [{
328
329
  kind: "selector",
329
330
  selector: "div[data-sidebar-right-panel]",
330
- optionalWhen: "noRightPanel"
331
+ optionalWhen: "rightPanelMounted",
332
+ visible: true
331
333
  }],
332
334
  sources: [{
333
335
  path: `${SIDEBAR_RIGHT}/lib/client.js`,
@@ -377,9 +379,10 @@ const HOST_CONTRACTS = [
377
379
  },
378
380
  target: "[data-dockkit-content]",
379
381
  checks: [{
380
- kind: "rule",
381
- match: "[data-dockkit-content]",
382
- hint: "dock tab host"
382
+ kind: "selector",
383
+ selector: "[data-dockkit-content]",
384
+ optionalWhen: "dockHasTab",
385
+ visible: true
383
386
  }],
384
387
  sources: [{
385
388
  path: ".",
@@ -404,9 +407,9 @@ const HOST_CONTRACTS = [
404
407
  },
405
408
  target: "[data-dockkit-empty]",
406
409
  checks: [{
407
- kind: "rule",
408
- match: "[data-dockkit-empty]",
409
- hint: "dock empty seat"
410
+ kind: "selector",
411
+ selector: "[data-dockkit-empty]",
412
+ optionalWhen: "dockHasNoTab"
410
413
  }],
411
414
  sources: [{
412
415
  path: `${SIDEBAR_RIGHT}/lib/client.js`,
@@ -430,9 +433,9 @@ const HOST_CONTRACTS = [
430
433
  },
431
434
  target: "[data-dockkit-float]",
432
435
  checks: [{
433
- kind: "rule",
434
- match: "[data-dockkit-float]",
435
- hint: "floating pane flag"
436
+ kind: "selector",
437
+ selector: "[data-dockkit-float]",
438
+ optionalWhen: "paneFloated"
436
439
  }],
437
440
  sources: [{
438
441
  path: `${SIDEBAR_RIGHT}/lib/client.js`,
@@ -458,7 +461,8 @@ const HOST_CONTRACTS = [
458
461
  checks: [{
459
462
  kind: "selector",
460
463
  selector: "[data-chat-flow]",
461
- optionalWhen: "chatNotMounted"
464
+ optionalWhen: "chatHasMessages",
465
+ visible: true
462
466
  }],
463
467
  sources: [{
464
468
  path: "dsh-client-ui-chat/lib/client.js",
@@ -484,7 +488,7 @@ const HOST_CONTRACTS = [
484
488
  checks: [{
485
489
  kind: "selector",
486
490
  selector: "[data-conversation-scroll]",
487
- optionalWhen: "chatNotMounted"
491
+ optionalWhen: "chatHasMessages"
488
492
  }],
489
493
  sources: [{
490
494
  path: "dsh-client-ui-conversation/lib/client.js",
@@ -510,7 +514,7 @@ const HOST_CONTRACTS = [
510
514
  checks: [{
511
515
  kind: "selector",
512
516
  selector: "[data-conversation-composer-overlay]",
513
- optionalWhen: "trajectoryNotMounted"
517
+ optionalWhen: "trajectoryMounted"
514
518
  }],
515
519
  sources: [{
516
520
  path: "dsh-client-ui-trajectory/lib/client.js",
package/lib/host-scan.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as thisPluginDir, c as DSH_FLOOR, i as resolveHostRoots, l as HOST_CONTRACTS, n as listHostPackages, o as versionOf, r as meetsFloor, t as checkHostOnDisk } from "./host-scan-WjK2VGBe.js";
1
+ import { a as thisPluginDir, c as DSH_FLOOR, i as resolveHostRoots, l as HOST_CONTRACTS, n as listHostPackages, o as versionOf, r as meetsFloor, t as checkHostOnDisk } from "./host-scan-C6rdxkf8.js";
2
2
  export { DSH_FLOOR, HOST_CONTRACTS, checkHostOnDisk, listHostPackages, meetsFloor, resolveHostRoots, thisPluginDir, versionOf };
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as checkHostOnDisk } from "./host-scan-WjK2VGBe.js";
1
+ import { t as checkHostOnDisk } from "./host-scan-C6rdxkf8.js";
2
2
  import { access, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
package/lib/testing.js CHANGED
@@ -1,4 +1,4 @@
1
- import { c as DSH_FLOOR, d as reportToMarkdown, f as symptomOf, i as resolveHostRoots, l as HOST_CONTRACTS, o as versionOf, p as verdictOf, r as meetsFloor, s as CONTRACT_GROUPS, t as checkHostOnDisk, u as labelOf } from "./host-scan-WjK2VGBe.js";
1
+ import { c as DSH_FLOOR, d as reportToMarkdown, f as symptomOf, i as resolveHostRoots, l as HOST_CONTRACTS, o as versionOf, p as verdictOf, r as meetsFloor, s as CONTRACT_GROUPS, t as checkHostOnDisk, u as labelOf } from "./host-scan-C6rdxkf8.js";
2
2
  const UI_CSS = `
3
3
  /* The section is rendered INLINE inside the host settings dialog's content
4
4
  * column: the host provides the modal chrome (backdrop, centering, closing).
@@ -288,24 +288,43 @@ function browserEnv(ctx) {
288
288
  if (host === null) return "";
289
289
  return getComputedStyle(host).getPropertyValue(token).trim();
290
290
  },
291
+ visible: (el) => {
292
+ if (el === null || typeof getComputedStyle !== "function") return true;
293
+ try {
294
+ return getComputedStyle(el).visibility !== "hidden";
295
+ } catch {
296
+ return true;
297
+ }
298
+ },
291
299
  styleSheets: () => typeof document === "undefined" ? [] : Array.from(document.styleSheets),
292
300
  ctx
293
301
  };
294
302
  }
295
303
  const ANCHORS = {
296
- settingsClosed: "div[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]",
297
- chatNotMounted: "[data-chat-flow],[data-conversation-scroll]",
298
- trajectoryNotMounted: "[data-conversation-composer-overlay]",
299
- noRightPanel: "div[data-sidebar-right-panel],[data-dockkit-strip],[data-dockkit-pane]",
300
- cordisClosed: "[data-cordis-panel]"
304
+ chatHasMessages: "[data-chat-turn]",
305
+ trajectoryMounted: "[data-conversation-composer-overlay]",
306
+ rightPanelMounted: "div[data-sidebar-right-panel],[data-dockkit-strip],[data-dockkit-pane]",
307
+ cordisMounted: "[data-cordis-panel]",
308
+ dockHasTab: "[data-dockkit-tab]",
309
+ dockHasNoTab: "[data-dockkit-empty],[data-dockkit-add-tab]",
310
+ paneFloated: "[data-dockkit-float],[data-sidebar-right-mode=\"float\"]",
311
+ settingsOpen: "div[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]"
301
312
  };
302
- function mounted(env, anchorId) {
313
+ /**
314
+ * Whether an anchor's surface is on screen.
315
+ *
316
+ * `optionalWhen: X` skips the check while this is FALSE — "the feature is not
317
+ * observable right now", which is what the field name promises. Every anchor is
318
+ * therefore written as a POSITIVE predicate (`dockHasTab`, `chatHasMessages`), and
319
+ * an anchor whose name reads as an absence would silently invert its check.
320
+ */
321
+ function anchorMounted(env, anchorId) {
303
322
  const sel = ANCHORS[anchorId];
304
323
  if (sel === void 0) return true;
305
324
  try {
306
325
  return env.querySelector(sel) !== null;
307
326
  } catch {
308
- return false;
327
+ return true;
309
328
  }
310
329
  }
311
330
  /** Walk a dotted path off a root object, tolerating a missing first hop. */
@@ -371,7 +390,9 @@ function ruleMentioned(env, literal, side) {
371
390
  }
372
391
  return false;
373
392
  }
374
- /** Which sides of stylesheet the probe can actually read (a blind check must not fail). */
393
+ /**
394
+ * Which sides of stylesheet the probe can actually read (a blind check must not fail).
395
+ */
375
396
  function readability(env) {
376
397
  const out = {
377
398
  host: false,
@@ -451,14 +472,14 @@ function evaluate(env, check) {
451
472
  };
452
473
  }
453
474
  case "selector": {
454
- const present = env.querySelector(check.selector) !== null;
475
+ const el = env.querySelector(check.selector);
455
476
  const name = `querySelector(${JSON.stringify(check.selector)})`;
456
- if (present) return {
477
+ if (el !== null && (check.visible !== true || env.visible(el))) return {
457
478
  status: "pass",
458
479
  name,
459
480
  detail: "element found"
460
481
  };
461
- if (check.optionalWhen !== void 0 && !mounted(env, check.optionalWhen)) return {
482
+ if (check.optionalWhen !== void 0 && !anchorMounted(env, check.optionalWhen)) return {
462
483
  status: "skip",
463
484
  name,
464
485
  detail: "not on screen right now"
@@ -466,8 +487,8 @@ function evaluate(env, check) {
466
487
  return {
467
488
  status: "fail",
468
489
  name,
469
- detail: "no element matches",
470
- reason: `no element matches ${check.selector} while its surface is on screen`
490
+ detail: el === null ? "no element matches" : "the element is hidden",
491
+ reason: el === null ? `no element matches ${check.selector} while its surface is on screen` : `${check.selector} is in the DOM but hidden while its surface is on screen`
471
492
  };
472
493
  }
473
494
  case "attr": {
@@ -478,7 +499,7 @@ function evaluate(env, check) {
478
499
  name,
479
500
  detail: "attribute found"
480
501
  };
481
- if (check.optionalWhen !== void 0 && !mounted(env, check.optionalWhen)) return {
502
+ if (check.optionalWhen !== void 0 && !anchorMounted(env, check.optionalWhen)) return {
482
503
  status: "skip",
483
504
  name,
484
505
  detail: "not on screen right now"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-background-by-model",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Appearance plugin for DeepSeek Harness: an ordered list of model rules, each carrying its own wallpaper, theme color, layout mode, opacity and blur 鈥?the background follows the model you switch to.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -1,19 +1,19 @@
1
1
  /**
2
- * The host self-check's own tests — run with
2
+ * The host self-check's own tests 鈥?run with
3
3
  *
4
4
  * pnpm test
5
5
  *
6
6
  * Three things are covered, and they are the three that can rot silently:
7
7
  *
8
- * 1. **The contract table holds together** — unique ids, machine-checkable
8
+ * 1. **The contract table holds together** 鈥?unique ids, machine-checkable
9
9
  * sources, a known criticality, and at least one check or an explicit
10
10
  * "documentation only" marker. A contract point added without a source would
11
11
  * otherwise be invisible to the offline scan.
12
- * 2. **The probe reports what is actually there** — the whole probe is driven
12
+ * 2. **The probe reports what is actually there** 鈥?the whole probe is driven
13
13
  * against a fixture written in the shape of a host DOM, so "the host renamed
14
14
  * `--dsw-specific-menu`" produces exactly the failure the panel claims, and a
15
15
  * surface that is merely off screen produces `n/a` rather than a false alarm.
16
- * 3. **The UI stylesheet, and the one piece of version logic** — every `dab-*`
16
+ * 3. **The UI stylesheet, and the one piece of version logic** 鈥?every `dab-*`
17
17
  * class a component uses is defined in it, and the floor comparison is exact
18
18
  * on the release triple.
19
19
  *
@@ -36,7 +36,7 @@ const { HOST_CONTRACTS, DSH_FLOOR, probeContracts, verdictOf, meetsFloor, UI_CSS
36
36
 
37
37
  const { readFile, readdir } = await import('node:fs/promises')
38
38
 
39
- // ── 1. the contract table ────────────────────────────────────────────────────
39
+ // 鈹€鈹€ 1. the contract table 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
40
40
 
41
41
  test('contract ids are unique and non-empty', () => {
42
42
  const seen = new Set()
@@ -75,30 +75,46 @@ test('critical contracts carry a check that can fail', () => {
75
75
  }
76
76
  })
77
77
 
78
- // ── 2. the probe, against a fixture in the shape of a host DOM ───────────────
78
+ // 鈹€鈹€ 2. the probe, against a fixture in the shape of a host DOM 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
79
79
 
80
80
  /** A DOM stand-in with just enough selector support for the contract checks. */
81
81
  function makeDom(host) {
82
- /** `selector → present`, including attribute and comma-OR forms. */
83
- const matches = sel => {
84
- for (const part of sel.split(',').map(p => p.trim())) {
85
- if (host.has(part)) return true
86
- const attr = /^\[([a-z-]+)\]$/.exec(part)
87
- if (attr !== null && host.has(`[${attr[1]}]`)) return true
88
- }
82
+ /**
83
+ * `selector 鈫?present`, including attribute and comma-OR forms. A descendant
84
+ * selector is satisfied when every compound in it is present 鈥?the stand-in has
85
+ * no tree, and the contracts are written so that "the elements exist" is the
86
+ * question being asked.
87
+ */
88
+ const one = part => {
89
+ // `*` is the universal selector: always satisfied.
90
+ if (part === '*') return true
91
+ if (host.has(part)) return true
92
+ const attr = /^\[([a-z-]+)\]$/.exec(part)
93
+ if (attr !== null && host.has(`[${attr[1]}]`)) return true
89
94
  return false
90
95
  }
96
+ const matches = sel => sel.split(',').map(p => p.trim()).some(
97
+ part => part.split(/\s+/).every(one),
98
+ )
91
99
  return {
92
- querySelector: sel => (matches(sel) ? { style: {} } : null),
100
+ // The element remembers which selector found it: a stand-in has no box to
101
+ // measure, and several real attributes live on ONE element (the dock's tab
102
+ // host carries both the populated marker and the parked empty seat), so
103
+ // "is this element visible" has to be answered per element, not per fixture.
104
+ querySelector: sel => (matches(sel) ? { style: {}, foundBy: sel } : null),
93
105
  // Where the host publishes its palette: `body` in the real client, and the
94
106
  // fixture models exactly that. Reading the document element instead is what
95
107
  // made seven healthy tokens report as empty.
96
108
  tokenRoot: () => ({ style: {} }),
97
109
  // `on` is null for an unscoped token check (the probe reads it off the token
98
110
  // root) and the resolved element otherwise. The fixture answers from its own
99
- // table either way — whether a value is readable is the host's own business,
111
+ // table either way 鈥?whether a value is readable is the host's own business,
100
112
  // and that is exactly what the table stands in for.
101
113
  computedValue: (token, _on) => (host.get(`computed:${token}`) ?? ''),
114
+ // A stand-in has no layout, so visibility is answered from an explicit table
115
+ // (`hidden:<selector>`). Anything unlisted counts as on screen 鈥?the
116
+ // conservative direction: an unanswerable question never fails a check.
117
+ visible: el => el === null || !host.has(`hidden:${el.foundBy}`),
102
118
  styleSheets: () => [],
103
119
  }
104
120
  }
@@ -111,7 +127,11 @@ const CORE_PRESENT = [
111
127
  '[data-composer-card]',
112
128
  '[data-cordis-panel]',
113
129
  '[data-chat-flow]',
130
+ '[data-chat-turn]',
114
131
  '[data-conversation-scroll]',
132
+ // The session root, so "no chat view" and "a chat view with nothing in it" can
133
+ // be told apart in the fixture the way they are in the host.
134
+ '[data-conversation-session]',
115
135
  '[data-conversation-composer-overlay]',
116
136
  '.dab-root',
117
137
  '[data-dockkit-strip]',
@@ -132,8 +152,8 @@ const PRESENT_TOKENS = [
132
152
 
133
153
  /**
134
154
  * The literals the HOST's stylesheets declare. The two halves of a token
135
- * contract ask different questions — `rule` = "does the host still declare this
136
- * name", `computed` = "does it resolve to a value" — so both are listed.
155
+ * contract ask different questions 鈥?`rule` = "does the host still declare this
156
+ * name", `computed` = "does it resolve to a value" 鈥?so both are listed.
137
157
  */
138
158
  const HOST_RULES = [
139
159
  '--dsw-menu-surface-fill', '--dsw-specific-menu', '--dsw-specific-sidebar-fill',
@@ -149,7 +169,7 @@ const OWN_RULES = ['.dab-card']
149
169
  /**
150
170
  * A stylesheet the probe may read.
151
171
  *
152
- * `own: true` writes this plugin's marker — the ONLY thing that tells our sheets
172
+ * `own: true` writes this plugin's marker 鈥?the ONLY thing that tells our sheets
153
173
  * apart from the host's, since the host's own CSS modules set `data-plugin` on
154
174
  * theirs, so a probe keying off that skips the entire host theme.
155
175
  *
@@ -185,19 +205,50 @@ function hostStyles(literals = HOST_RULES, ownLiterals = OWN_RULES) {
185
205
  /**
186
206
  * A fixture host: `selectors` are the ones that match, `rules` the literals the
187
207
  * host stylesheet declares, and every token in `PRESENT_TOKENS` resolves unless
188
- * the caller drops it.
208
+ * the caller drops it. `hidden` lists selectors that are present but parked off
209
+ * screen (`visibility:hidden`) 鈥?the state the dock's empty seat sits in.
189
210
  */
190
- function hostEnv({ ctx, selectors = CORE_PRESENT, rules = HOST_RULES, tokens = PRESENT_TOKENS }) {
211
+ function hostEnv({ ctx, selectors = CORE_PRESENT, rules = HOST_RULES, tokens = PRESENT_TOKENS, hidden = [] }) {
191
212
  // A Map, not a Set: the fixture must answer with the token's VALUE, and only a
192
213
  // Map carries one (a Set has no `get`).
193
214
  const host = new Map()
194
215
  for (const s of selectors) host.set(s, true)
216
+ for (const s of hidden) host.set(`hidden:${s}`, true)
195
217
  for (const t of tokens) host.set(`computed:${t}`, '#123456')
196
218
  return { ...makeDom(host), styleSheets: () => hostStyles(rules), ctx }
197
219
  }
198
220
 
199
- /** The healthy fixture: nothing missing, every service published. */
200
- const healthyEnv = () => hostEnv({ ctx: healthyCtx() })
221
+ /**
222
+ * The two states the dock alternates between. In the real DOM the tab host and
223
+ * the empty seat are different elements, and each contract's anchor is what says
224
+ * whether its own attribute can be observed — so the fixture provides the anchor
225
+ * for every contract it expects to answer, and omits the anchors of the ones it
226
+ * expects to read as n/a.
227
+ *
228
+ * The panel selector is listed twice on purpose: the fixture matches selectors by
229
+ * exact text (it has no tree to walk), and the contracts spell it both as
230
+ * `[data-sidebar-right-panel]` (the anchor) and `div[data-sidebar-right-panel]`
231
+ * (the check).
232
+ */
233
+ const DOCK_POPULATED = [
234
+ '[data-sidebar-right-panel]',
235
+ 'div[data-sidebar-right-panel]',
236
+ '[data-dockkit-tab]',
237
+ '[data-dockkit-content]',
238
+ '[data-dockkit-empty]',
239
+ ]
240
+ const DOCK_EMPTY = [
241
+ '[data-sidebar-right-panel]',
242
+ 'div[data-sidebar-right-panel]',
243
+ '[data-dockkit-empty]',
244
+ ]
245
+
246
+ /** The healthy fixtures: nothing missing, every service published. */
247
+ const healthyEnv = (dock = DOCK_POPULATED, extra = {}) => hostEnv({
248
+ ...extra,
249
+ ctx: healthyCtx(),
250
+ selectors: [...CORE_PRESENT, ...dock],
251
+ })
201
252
 
202
253
  /** A context publishing the four services the model hop needs. */
203
254
  function healthyCtx(overrides = {}) {
@@ -211,11 +262,14 @@ function healthyCtx(overrides = {}) {
211
262
  return { get: id => services[id] }
212
263
  }
213
264
 
214
- test('a healthy host passes every contract', () => {
215
- const results = probeContracts(healthyEnv(), 'en')
216
- const failed = results.filter(r => r.status === 'fail')
217
- assert.deepEqual(failed.map(r => r.id), [], `unexpected failures: ${failed.map(r => `${r.id}: ${r.reason}`).join('; ')}`)
218
- assert.equal(verdictOf({ results, summary: { pass: results.length, fail: 0, skip: 0, total: results.length } }), 'ok')
265
+ test('a healthy host passes every contract, in either dock state', () => {
266
+ for (const [name, dock] of [['populated tab', DOCK_POPULATED], ['empty seat', DOCK_EMPTY]]) {
267
+ const results = probeContracts(healthyEnv(dock), 'en')
268
+ const failed = results.filter(r => r.status === 'fail')
269
+ assert.deepEqual(failed.map(r => r.id), [],
270
+ `unexpected failures with a ${name}: ${failed.map(r => `${r.id}: ${r.reason}`).join('; ')}`)
271
+ assert.equal(verdictOf({ results, summary: { pass: results.length, fail: 0, skip: 0, total: results.length } }), 'ok')
272
+ }
219
273
  })
220
274
 
221
275
  test('a renamed menu token fails exactly that contract, and names the symptom', () => {
@@ -232,8 +286,8 @@ test('a renamed menu token fails exactly that contract, and names the symptom',
232
286
 
233
287
  test('a renamed token the host still aliases is caught by the rule half', () => {
234
288
  // The 0.1.7 menu split in reverse: the host keeps the OLD name working (aliased)
235
- // but stops declaring the new one. `computed` still answers — which is exactly
236
- // how that bug stayed silent — so the `rule` half is the only thing that can
289
+ // but stops declaring the new one. `computed` still answers 鈥?which is exactly
290
+ // how that bug stayed silent 鈥?so the `rule` half is the only thing that can
237
291
  // tell "renamed" from "still there".
238
292
  const env = hostEnv({
239
293
  ctx: healthyCtx(),
@@ -248,7 +302,7 @@ test('a renamed token the host still aliases is caught by the rule half', () =>
248
302
 
249
303
  test('a host that stops publishing the palette fails the computed half', () => {
250
304
  // The other failure shape: the name is still declared somewhere, but body no
251
- // longer carries a value — the case that made 7 healthy tokens look empty when
305
+ // longer carries a value 鈥?the case that made 7 healthy tokens look empty when
252
306
  // the probe read the wrong element.
253
307
  const env = hostEnv({
254
308
  ctx: healthyCtx(),
@@ -270,7 +324,7 @@ test('a dropped session service fails the model hop', () => {
270
324
  test('the plugin\'s own token stylesheet cannot make a host check pass', () => {
271
325
  // The situation that produced the 0.1.7 white band: the plugin re-emits the
272
326
  // menu fill itself, so its own sheet declares the token. That re-emission must
273
- // never be accepted as evidence about the HOST — otherwise the check is green
327
+ // never be accepted as evidence about the HOST 鈥?otherwise the check is green
274
328
  // by construction on exactly the host where the token was renamed away.
275
329
  const env = hostEnv({
276
330
  ctx: healthyCtx(),
@@ -287,7 +341,7 @@ test('the plugin\'s own token stylesheet cannot make a host check pass', () => {
287
341
  assert.equal(byId('token.menuSurface').status, 'fail')
288
342
  assert.match(byId('token.menuSurface').reason, /no host stylesheet declares/)
289
343
  assert.equal(byId('token.menuAlias').status, 'fail')
290
- // …while the contract whose other half IS our own sheet still passes: this is
344
+ // 鈥hile the contract whose other half IS our own sheet still passes: this is
291
345
  // what `side: 'own'` is for, and why the two sides must never be merged.
292
346
  assert.equal(byId('settings.card').status, 'pass')
293
347
  })
@@ -304,9 +358,151 @@ test('a host sheet carrying data-plugin is not mistaken for ours', () => {
304
358
  assert.equal(results.find(r => r.id === 'token.bgBase').status, 'pass')
305
359
  })
306
360
 
361
+ test('a closed dock holding only its empty seat is n/a, not a failure', () => {
362
+ // The state a healthy host sits in most of the time: the dock holds no tab, so
363
+ // `data-dockkit-content` (a JS-set attribute the host never styles) cannot be
364
+ // observed at all. It used to read as "no host stylesheet declares it" 鈥?a
365
+ // perfectly healthy host reported as broken.
366
+ const env = healthyEnv(DOCK_EMPTY)
367
+ const results = probeContracts(env, 'en')
368
+ const byId = id => results.find(r => r.id === id)
369
+ assert.equal(byId('dockkit.content').status, 'skip')
370
+ // The empty seat IS the thing on screen here, so its own contract answers — it
371
+ // is present (the host renders it visibly in this state; it is only parked and
372
+ // hidden once a tab opens).
373
+ assert.equal(byId('dockkit.empty').status, 'pass')
374
+ assert.equal(byId('dockkit.float').status, 'skip')
375
+ assert.deepEqual(results.filter(r => r.status === 'fail').map(r => r.id), [])
376
+ })
377
+
378
+ test('a populated dock reports its empty seat as n/a, not as missing', () => {
379
+ // The state the panel showed the user a red line for. With a tab open the host
380
+ // renders no seat on that screen at all — the seat is not the thing being shown —
381
+ // so the question has no answer there, which is n/a and must never be a failure.
382
+ const env = hostEnv({
383
+ ctx: healthyCtx(),
384
+ selectors: [...CORE_PRESENT, 'div[data-sidebar-right-panel]', '[data-dockkit-tab]', '[data-dockkit-content]'],
385
+ })
386
+ const results = probeContracts(env, 'en')
387
+ const byId = id => results.find(r => r.id === id)
388
+ assert.equal(byId('dockkit.empty').status, 'skip')
389
+ assert.equal(byId('dockkit.content').status, 'pass')
390
+ assert.deepEqual(results.filter(r => r.status === 'fail').map(r => r.id), [])
391
+ })
392
+
393
+ test('a parked empty seat is still a pass, not a failure', () => {
394
+ // Presence is the whole question. The host parks the seat with
395
+ // `visibility:hidden` as tabs come and go, so asking whether it is VISIBLE is
396
+ // asking about a state the host uses both ways — which is how this contract ended
397
+ // up reporting a red line on a healthy dock.
398
+ const env = hostEnv({
399
+ ctx: healthyCtx(),
400
+ selectors: [...CORE_PRESENT, 'div[data-sidebar-right-panel]', '[data-dockkit-add-tab]', '[data-dockkit-empty]'],
401
+ hidden: ['[data-dockkit-empty]'],
402
+ })
403
+ const results = probeContracts(env, 'en')
404
+ assert.equal(results.find(r => r.id === 'dockkit.empty').status, 'pass')
405
+ })
406
+
407
+ test('a seat that is gone while the dock is empty fails', () => {
408
+ // The real rename, and the one way this contract can fail: the dock is on screen
409
+ // with the host's "add a tab" button — so it holds no tab — and the seat is not
410
+ // there. (The old form tested the opposite state, a populated dock, instead.)
411
+ const env = hostEnv({
412
+ ctx: healthyCtx(),
413
+ selectors: [...CORE_PRESENT, 'div[data-sidebar-right-panel]', '[data-dockkit-add-tab]'],
414
+ })
415
+ const results = probeContracts(env, 'en')
416
+ const row = results.find(r => r.id === 'dockkit.empty')
417
+ assert.equal(row.status, 'fail')
418
+ assert.match(row.reason, /no element matches/)
419
+ })
420
+
421
+ test('a surface that is mounted and empty is n/a, not a renamed marker', () => {
422
+ // The hero screen: a session is open, the composer is there, and the host has
423
+ // deliberately rendered no message column because there are no messages. The
424
+ // previous anchor ("the conversation scrollport exists") is true here, so
425
+ // `[data-chat-flow]` was reported as renamed on a healthy, brand-new session.
426
+ const env = hostEnv({
427
+ ctx: healthyCtx(),
428
+ selectors: ['[data-conversation-session]', '[data-conversation-scroll]', '[data-composer-card]',
429
+ '[data-content-phase]', '[data-shell-overlay]', '.dab-root', ...DOCK_EMPTY],
430
+ })
431
+ const results = probeContracts(env, 'en')
432
+ const byId = id => results.find(r => r.id === id)
433
+ assert.equal(byId('chat.flow').status, 'skip')
434
+ // The two markers that ARE expected in that phase still have to answer, or this
435
+ // guard would hide a real rename behind "the chat is not mounted".
436
+ assert.equal(byId('composer.card').status, 'pass')
437
+ assert.equal(byId('conversation.scroll').status, 'pass')
438
+ assert.deepEqual(results.filter(r => r.status === 'fail').map(r => r.id), [])
439
+ })
440
+
441
+ test('the trajectory tab is n/a for the chat column, not a failure', () => {
442
+ // Measured on the running host: the trajectory view unmounts the chat column
443
+ // (`data-trajectory-scroll` in, `data-chat-flow` out) while the session root
444
+ // changes phase rather than disappearing. Switching to it used to report
445
+ // "no element matches [data-chat-flow] while its surface is on screen".
446
+ const env = hostEnv({
447
+ ctx: healthyCtx(),
448
+ selectors: ['[data-conversation-session]', '[data-content-phase]', '[data-conversation-scroll]',
449
+ '[data-composer-card]', '[data-conversation-composer-overlay]', '[data-shell-overlay]', '.dab-root',
450
+ ...DOCK_POPULATED],
451
+ })
452
+ const results = probeContracts(env, 'en')
453
+ const byId = id => results.find(r => r.id === id)
454
+ assert.equal(byId('chat.flow').status, 'skip')
455
+ assert.equal(byId('trajectory.root').status, 'pass')
456
+ assert.deepEqual(results.filter(r => r.status === 'fail').map(r => r.id), [])
457
+ })
458
+
459
+ test('a message column that is present but hidden is a failure', () => {
460
+ // The other half of the fix: `visible` must not soften the check into "it is
461
+ // somewhere in the DOM". A column the user cannot see is not a column the card
462
+ // can be drawn on, and the report has to say which of the two happened.
463
+ const env = hostEnv({
464
+ ctx: healthyCtx(),
465
+ selectors: [...CORE_PRESENT, ...DOCK_POPULATED],
466
+ hidden: ['[data-chat-flow]'],
467
+ })
468
+ const results = probeContracts(env, 'en')
469
+ const row = results.find(r => r.id === 'chat.flow')
470
+ assert.equal(row.status, 'fail')
471
+ assert.match(row.reason, /hidden/)
472
+ })
473
+
474
+ test('a renamed dock tab attribute still fails once a tab is open', () => {
475
+ // The guard must not swallow a real rename: with a tab open the host has to
476
+ // carry `data-dockkit-content` on the tab host, and if the name moved, say so.
477
+ // A populated tab is on screen (the host marks the tab chip), but the attribute
478
+ // the contract names no longer matches anything.
479
+ const env = hostEnv({
480
+ ctx: healthyCtx(),
481
+ selectors: ['div[data-sidebar-right-panel]', '[data-dockkit-tab]'],
482
+ })
483
+ const results = probeContracts(env, 'en')
484
+ const row = results.find(r => r.id === 'dockkit.content')
485
+ assert.equal(row.status, 'fail')
486
+ assert.match(row.reason, /data-dockkit-content/)
487
+ })
488
+
489
+ test('a floating pane missing its flag is a failure while it is on screen', () => {
490
+ // The float flag is a JS attribute too, so it is only observable when the user
491
+ // has a pane floated 鈥?and then, if the host stopped setting it, the plugin
492
+ // would paint that pane like a docked one.
493
+ const env = hostEnv({
494
+ ctx: healthyCtx(),
495
+ selectors: [...CORE_PRESENT, ...DOCK_POPULATED, '[data-sidebar-right-mode="float"]'],
496
+ })
497
+ const results = probeContracts(env, 'en')
498
+ const row = results.find(r => r.id === 'dockkit.float')
499
+ assert.equal(row.status, 'fail')
500
+ assert.match(row.reason, /data-dockkit-float/)
501
+ })
502
+
307
503
  test('a surface that is merely off screen is n/a, not a failure', () => {
308
504
  // The app frame and every service are up (the settings section is rendered
309
- // inside the frame, so the frame is always there) — but the conversation view,
505
+ // inside the frame, so the frame is always there) 鈥?but the conversation view,
310
506
  // the right panel and the Cordis panel are not. Nothing is broken; those
311
507
  // surfaces simply are not there to observe.
312
508
  const env = hostEnv({
@@ -326,7 +522,7 @@ test('a surface that is merely off screen is n/a, not a failure', () => {
326
522
  })
327
523
 
328
524
  test('a mounted but marker-less surface is a failure, not n/a', () => {
329
- // The dialog is open, so the right panel's anchor is off screen — but the
525
+ // The dialog is open, so the right panel's anchor is off screen 鈥?but the
330
526
  // dialog's own selector still matches. A surface whose anchor IS on screen and
331
527
  // whose selector is not is a rename, and must not read as "not observable".
332
528
  const env = hostEnv({
@@ -342,7 +538,7 @@ test('a mounted but marker-less surface is a failure, not n/a', () => {
342
538
  assert.equal(byId('chat.flow').status, 'skip')
343
539
  })
344
540
 
345
- // ── 3. the settings UI's own stylesheet ──────────────────────────────────────
541
+ // 鈹€鈹€ 3. the settings UI's own stylesheet 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
346
542
 
347
543
  test('every dab-* class a component uses is defined in the UI stylesheet', async () => {
348
544
  const dir = join(pluginDir, 'src', 'client')
@@ -372,7 +568,7 @@ test('every dab-* class a component uses is defined in the UI stylesheet', async
372
568
  assert.deepEqual(missing, [], `classes used but never styled: ${missing.map(m => `${m.cls} (${m.file})`).join(', ')}`)
373
569
  })
374
570
 
375
- // ── 4. the floor comparison ──────────────────────────────────────────────────
571
+ // 鈹€鈹€ 4. the floor comparison 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
376
572
 
377
573
  test('the floor comparison is coarse on purpose and exact on the triple', () => {
378
574
  assert.equal(meetsFloor('0.1.7-rc.2', DSH_FLOOR), true, 'the floor itself satisfies the floor')