dsh-background-by-model 0.4.2 → 0.4.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-background-by-model",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
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
- /**
2
- * The host self-check's own tests — run with
1
+ /**
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
  *
@@ -31,12 +31,12 @@ const here = dirname(fileURLToPath(import.meta.url))
31
31
  const pluginDir = resolve(here, '..')
32
32
  const testingPath = join(pluginDir, 'lib', 'testing.js')
33
33
 
34
- const { HOST_CONTRACTS, DSH_FLOOR, probeContracts, verdictOf, meetsFloor, UI_CSS } =
34
+ const { HOST_CONTRACTS, DSH_FLOOR, probeContracts, verdictOf, meetsFloor, UI_CSS, OWN_SHEET_ATTR, OWN_SHEET_DATASET } =
35
35
  await import(pathToFileURL(testingPath).href)
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,25 +75,37 @@ 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
100
  querySelector: sel => (matches(sel) ? { style: {} } : null),
93
- // `on` is null for an unscoped token check (the probe reads it off the
94
- // document root) and the resolved element otherwise. The fixture answers from
95
- // its own table either way — whether a value is readable is the host's own
96
- // business, and that is exactly what the table stands in for.
101
+ // Where the host publishes its palette: `body` in the real client, and the
102
+ // fixture models exactly that. Reading the document element instead is what
103
+ // made seven healthy tokens report as empty.
104
+ tokenRoot: () => ({ style: {} }),
105
+ // `on` is null for an unscoped token check (the probe reads it off the token
106
+ // root) and the resolved element otherwise. The fixture answers from its own
107
+ // table either way 鈥?whether a value is readable is the host's own business,
108
+ // and that is exactly what the table stands in for.
97
109
  computedValue: (token, _on) => (host.get(`computed:${token}`) ?? ''),
98
110
  styleSheets: () => [],
99
111
  }
@@ -127,21 +139,55 @@ const PRESENT_TOKENS = [
127
139
  ]
128
140
 
129
141
  /**
130
- * A host that also declares every token/class/attribute rule the contracts look
131
- * for — a complete stylesheet fixture minus whatever a test removes.
142
+ * The literals the HOST's stylesheets declare. The two halves of a token
143
+ * contract ask different questions 鈥?`rule` = "does the host still declare this
144
+ * name", `computed` = "does it resolve to a value" 鈥?so both are listed.
132
145
  */
133
146
  const HOST_RULES = [
134
147
  '--dsw-menu-surface-fill', '--dsw-specific-menu', '--dsw-specific-sidebar-fill',
135
148
  '--dsw-specific-input-major', '--dsw-alias-bg-base', '--dsw-alias-bg-layer-1',
136
- '.dab-card', '.md-table-wide', '[data-dockkit-content]', '[data-dockkit-empty]',
149
+ '--dsw-alias-label-caption',
150
+ '.md-table-wide', '[data-dockkit-content]', '[data-dockkit-empty]',
137
151
  '[data-dockkit-float]', '[data-sidebar-right-open]', 'data-ds-dark-theme',
138
152
  ]
139
153
 
140
- function hostStyles(literals = HOST_RULES) {
141
- return [{
142
- ownerNode: { dataset: {} },
154
+ /** The one literal only THIS plugin's stylesheet declares (`.dab-card`, `side: 'own'`). */
155
+ const OWN_RULES = ['.dab-card']
156
+
157
+ /**
158
+ * A stylesheet the probe may read.
159
+ *
160
+ * `own: true` writes this plugin's marker 鈥?the ONLY thing that tells our sheets
161
+ * apart from the host's, since the host's own CSS modules set `data-plugin` on
162
+ * theirs, so a probe keying off that skips the entire host theme.
163
+ *
164
+ * The fixture exposes the attribute the way a REAL element does: `getAttribute`
165
+ * answers the dashed name, and `dataset` only the camel-cased key. Modelling this
166
+ * wrong is how a probe that reads `dataset['data-dab-side']` passes here and
167
+ * silently fails in a browser.
168
+ */
169
+ function sheet(literals, own = false) {
170
+ const attrs = {}
171
+ const dataset = {}
172
+ if (own) {
173
+ attrs[OWN_SHEET_ATTR] = 'own'
174
+ dataset[OWN_SHEET_DATASET] = 'own'
175
+ }
176
+ return {
177
+ ownerNode: {
178
+ dataset,
179
+ getAttribute: name => (name in attrs ? attrs[name] : null),
180
+ setAttribute: (name, value) => { attrs[name] = value },
181
+ },
143
182
  cssRules: literals.map(l => ({ cssText: `.x{color:red}/* ${l} */` })),
144
- }]
183
+ }
184
+ }
185
+
186
+ /** The host's sheets plus our own, minus whatever a test removes. */
187
+ function hostStyles(literals = HOST_RULES, ownLiterals = OWN_RULES) {
188
+ const sheets = [sheet(literals)]
189
+ if (ownLiterals.length > 0) sheets.push(sheet(ownLiterals, true))
190
+ return sheets
145
191
  }
146
192
 
147
193
  /**
@@ -158,8 +204,27 @@ function hostEnv({ ctx, selectors = CORE_PRESENT, rules = HOST_RULES, tokens = P
158
204
  return { ...makeDom(host), styleSheets: () => hostStyles(rules), ctx }
159
205
  }
160
206
 
161
- /** The healthy fixture: nothing missing, every service published. */
162
- const healthyEnv = () => hostEnv({ ctx: healthyCtx() })
207
+ /**
208
+ * The two states the dock alternates between. In the real DOM they live in
209
+ * different panes (a populated tab host carries `data-dockkit-content`, an empty
210
+ * pane carries `data-dockkit-empty`), and each contract's anchor is what says
211
+ * whether its own attribute can be observed 鈥?so the fixture provides the anchor
212
+ * for every contract it expects to answer, and omits the anchors of the ones it
213
+ * expects to read as n/a.
214
+ */
215
+ const DOCK_POPULATED = [
216
+ 'div[data-sidebar-right-panel]',
217
+ '[data-dockkit-tab]',
218
+ '[data-dockkit-content]',
219
+ '[data-dockkit-empty]',
220
+ ]
221
+ const DOCK_EMPTY = ['div[data-sidebar-right-panel]', '[data-dockkit-empty]']
222
+
223
+ /** The healthy fixtures: nothing missing, every service published. */
224
+ const healthyEnv = (dock = DOCK_POPULATED) => hostEnv({
225
+ ctx: healthyCtx(),
226
+ selectors: [...CORE_PRESENT, ...dock],
227
+ })
163
228
 
164
229
  /** A context publishing the four services the model hop needs. */
165
230
  function healthyCtx(overrides = {}) {
@@ -173,11 +238,14 @@ function healthyCtx(overrides = {}) {
173
238
  return { get: id => services[id] }
174
239
  }
175
240
 
176
- test('a healthy host passes every contract', () => {
177
- const results = probeContracts(healthyEnv(), 'en')
178
- const failed = results.filter(r => r.status === 'fail')
179
- assert.deepEqual(failed.map(r => r.id), [], `unexpected failures: ${failed.map(r => `${r.id}: ${r.reason}`).join('; ')}`)
180
- assert.equal(verdictOf({ results, summary: { pass: results.length, fail: 0, skip: 0, total: results.length } }), 'ok')
241
+ test('a healthy host passes every contract, in either dock state', () => {
242
+ for (const [name, dock] of [['populated tab', DOCK_POPULATED], ['empty seat', DOCK_EMPTY]]) {
243
+ const results = probeContracts(healthyEnv(dock), 'en')
244
+ const failed = results.filter(r => r.status === 'fail')
245
+ assert.deepEqual(failed.map(r => r.id), [],
246
+ `unexpected failures with a ${name}: ${failed.map(r => `${r.id}: ${r.reason}`).join('; ')}`)
247
+ assert.equal(verdictOf({ results, summary: { pass: results.length, fail: 0, skip: 0, total: results.length } }), 'ok')
248
+ }
181
249
  })
182
250
 
183
251
  test('a renamed menu token fails exactly that contract, and names the symptom', () => {
@@ -192,6 +260,36 @@ test('a renamed menu token fails exactly that contract, and names the symptom',
192
260
  assert.match(row.symptom, /solid band|white band|sticky/i)
193
261
  })
194
262
 
263
+ test('a renamed token the host still aliases is caught by the rule half', () => {
264
+ // The 0.1.7 menu split in reverse: the host keeps the OLD name working (aliased)
265
+ // but stops declaring the new one. `computed` still answers 鈥?which is exactly
266
+ // how that bug stayed silent 鈥?so the `rule` half is the only thing that can
267
+ // tell "renamed" from "still there".
268
+ const env = hostEnv({
269
+ ctx: healthyCtx(),
270
+ rules: HOST_RULES.filter(l => l !== '--dsw-menu-surface-fill'),
271
+ })
272
+ const results = probeContracts(env, 'en')
273
+ const row = results.find(r => r.id === 'token.menuSurface')
274
+ assert.equal(row.status, 'fail')
275
+ // The computed half would have passed: the token still resolves a value.
276
+ assert.match(row.detail, /host stylesheet includes/)
277
+ })
278
+
279
+ test('a host that stops publishing the palette fails the computed half', () => {
280
+ // The other failure shape: the name is still declared somewhere, but body no
281
+ // longer carries a value 鈥?the case that made 7 healthy tokens look empty when
282
+ // the probe read the wrong element.
283
+ const env = hostEnv({
284
+ ctx: healthyCtx(),
285
+ tokens: PRESENT_TOKENS.filter(t => t !== '--dsw-alias-bg-base'),
286
+ })
287
+ const results = probeContracts(env, 'en')
288
+ const row = results.find(r => r.id === 'token.bgBase')
289
+ assert.equal(row.status, 'fail')
290
+ assert.match(row.reason, /--dsw-alias-bg-base/)
291
+ })
292
+
195
293
  test('a dropped session service fails the model hop', () => {
196
294
  const env = hostEnv({ ctx: healthyCtx({ sessions: undefined }) })
197
295
  const results = probeContracts(env, 'en')
@@ -200,24 +298,89 @@ test('a dropped session service fails the model hop', () => {
200
298
  })
201
299
 
202
300
  test('the plugin\'s own token stylesheet cannot make a host check pass', () => {
301
+ // The situation that produced the 0.1.7 white band: the plugin re-emits the
302
+ // menu fill itself, so its own sheet declares the token. That re-emission must
303
+ // never be accepted as evidence about the HOST 鈥?otherwise the check is green
304
+ // by construction on exactly the host where the token was renamed away.
203
305
  const env = hostEnv({
204
306
  ctx: healthyCtx(),
205
- // The ONLY stylesheet is the plugin's own — exactly the situation that
206
- // produced the 0.1.7 white band while the check would have been green.
307
+ // The host sheet is readable but has moved on: it no longer declares the
308
+ // menu fill under this name.
207
309
  rules: [],
208
310
  })
209
- env.styleSheets = () => [{
210
- ownerNode: { dataset: { plugin: 'dsh-background-by-model-tokens' } },
211
- cssRules: [{ cssText: 'body{--dsw-menu-surface-fill:red!important;--dsw-specific-menu:red!important}' }],
212
- }]
311
+ env.styleSheets = () => [
312
+ sheet([]), // the host's own, readable, declaring nothing
313
+ sheet(['--dsw-menu-surface-fill', '--dsw-specific-menu', '.dab-card'], true),
314
+ ]
315
+ const results = probeContracts(env, 'en')
316
+ const byId = id => results.find(r => r.id === id)
317
+ assert.equal(byId('token.menuSurface').status, 'fail')
318
+ assert.match(byId('token.menuSurface').reason, /no host stylesheet declares/)
319
+ assert.equal(byId('token.menuAlias').status, 'fail')
320
+ // 鈥hile the contract whose other half IS our own sheet still passes: this is
321
+ // what `side: 'own'` is for, and why the two sides must never be merged.
322
+ assert.equal(byId('settings.card').status, 'pass')
323
+ })
324
+
325
+ test('a host sheet carrying data-plugin is not mistaken for ours', () => {
326
+ // Regression for the probe bug that reported 7 healthy tokens as renamed: the
327
+ // host's CSS modules ALSO set `data-plugin` on their <style> elements, so a
328
+ // probe that skips "sheets with data-plugin" skips the host's whole theme.
329
+ const hostSheet = sheet(['--dsw-alias-bg-base'])
330
+ hostSheet.ownerNode.dataset.plugin = '@deepseek-ai/dsh-client-ui-theme'
331
+ const env = hostEnv({ ctx: healthyCtx(), rules: [] })
332
+ env.styleSheets = () => [hostSheet]
333
+ const results = probeContracts(env, 'en')
334
+ assert.equal(results.find(r => r.id === 'token.bgBase').status, 'pass')
335
+ })
336
+
337
+ test('a closed dock holding only its empty seat is n/a, not a failure', () => {
338
+ // The state a healthy host sits in most of the time: the dock holds no tab, so
339
+ // `data-dockkit-content` (a JS-set attribute the host never styles) cannot be
340
+ // observed at all. It used to read as "no host stylesheet declares it" 鈥?a
341
+ // perfectly healthy host reported as broken.
342
+ const env = healthyEnv(DOCK_EMPTY)
343
+ const results = probeContracts(env, 'en')
344
+ const byId = id => results.find(r => r.id === id)
345
+ assert.equal(byId('dockkit.content').status, 'skip')
346
+ // The empty seat IS on screen here, so its own contract answers.
347
+ assert.equal(byId('dockkit.empty').status, 'pass')
348
+ assert.equal(byId('dockkit.float').status, 'skip')
349
+ assert.deepEqual(results.filter(r => r.status === 'fail').map(r => r.id), [])
350
+ })
351
+
352
+ test('a renamed dock tab attribute still fails once a tab is open', () => {
353
+ // The guard must not swallow a real rename: with a tab open the host has to
354
+ // carry `data-dockkit-content` on the tab host, and if the name moved, say so.
355
+ // A populated tab is on screen (the host marks the tab chip), but the attribute
356
+ // the contract names no longer matches anything.
357
+ const env = hostEnv({
358
+ ctx: healthyCtx(),
359
+ selectors: ['div[data-sidebar-right-panel]', '[data-dockkit-tab]'],
360
+ })
361
+ const results = probeContracts(env, 'en')
362
+ const row = results.find(r => r.id === 'dockkit.content')
363
+ assert.equal(row.status, 'fail')
364
+ assert.match(row.reason, /data-dockkit-content/)
365
+ })
366
+
367
+ test('a floating pane missing its flag is a failure while it is on screen', () => {
368
+ // The float flag is a JS attribute too, so it is only observable when the user
369
+ // has a pane floated 鈥?and then, if the host stopped setting it, the plugin
370
+ // would paint that pane like a docked one.
371
+ const env = hostEnv({
372
+ ctx: healthyCtx(),
373
+ selectors: [...CORE_PRESENT, ...DOCK_POPULATED, '[data-sidebar-right-mode="float"]'],
374
+ })
213
375
  const results = probeContracts(env, 'en')
214
- assert.equal(results.find(r => r.id === 'token.menuSurface').status, 'fail')
215
- assert.equal(results.find(r => r.id === 'token.menuAlias').status, 'fail')
376
+ const row = results.find(r => r.id === 'dockkit.float')
377
+ assert.equal(row.status, 'fail')
378
+ assert.match(row.reason, /data-dockkit-float/)
216
379
  })
217
380
 
218
381
  test('a surface that is merely off screen is n/a, not a failure', () => {
219
382
  // The app frame and every service are up (the settings section is rendered
220
- // inside the frame, so the frame is always there) — but the conversation view,
383
+ // inside the frame, so the frame is always there) 鈥?but the conversation view,
221
384
  // the right panel and the Cordis panel are not. Nothing is broken; those
222
385
  // surfaces simply are not there to observe.
223
386
  const env = hostEnv({
@@ -237,7 +400,7 @@ test('a surface that is merely off screen is n/a, not a failure', () => {
237
400
  })
238
401
 
239
402
  test('a mounted but marker-less surface is a failure, not n/a', () => {
240
- // The dialog is open, so the right panel's anchor is off screen — but the
403
+ // The dialog is open, so the right panel's anchor is off screen 鈥?but the
241
404
  // dialog's own selector still matches. A surface whose anchor IS on screen and
242
405
  // whose selector is not is a rename, and must not read as "not observable".
243
406
  const env = hostEnv({
@@ -253,7 +416,7 @@ test('a mounted but marker-less surface is a failure, not n/a', () => {
253
416
  assert.equal(byId('chat.flow').status, 'skip')
254
417
  })
255
418
 
256
- // ── 3. the settings UI's own stylesheet ──────────────────────────────────────
419
+ // 鈹€鈹€ 3. the settings UI's own stylesheet 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
257
420
 
258
421
  test('every dab-* class a component uses is defined in the UI stylesheet', async () => {
259
422
  const dir = join(pluginDir, 'src', 'client')
@@ -283,7 +446,7 @@ test('every dab-* class a component uses is defined in the UI stylesheet', async
283
446
  assert.deepEqual(missing, [], `classes used but never styled: ${missing.map(m => `${m.cls} (${m.file})`).join(', ')}`)
284
447
  })
285
448
 
286
- // ── 4. the floor comparison ──────────────────────────────────────────────────
449
+ // 鈹€鈹€ 4. the floor comparison 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
287
450
 
288
451
  test('the floor comparison is coarse on purpose and exact on the triple', () => {
289
452
  assert.equal(meetsFloor('0.1.7-rc.2', DSH_FLOOR), true, 'the floor itself satisfies the floor')