dsh-background-by-model 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.3",
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",
@@ -31,7 +31,7 @@ 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')
@@ -90,10 +90,14 @@ function makeDom(host) {
90
90
  }
91
91
  return {
92
92
  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.
93
+ // Where the host publishes its palette: `body` in the real client, and the
94
+ // fixture models exactly that. Reading the document element instead is what
95
+ // made seven healthy tokens report as empty.
96
+ tokenRoot: () => ({ style: {} }),
97
+ // `on` is null for an unscoped token check (the probe reads it off the token
98
+ // 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,
100
+ // and that is exactly what the table stands in for.
97
101
  computedValue: (token, _on) => (host.get(`computed:${token}`) ?? ''),
98
102
  styleSheets: () => [],
99
103
  }
@@ -127,21 +131,55 @@ const PRESENT_TOKENS = [
127
131
  ]
128
132
 
129
133
  /**
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.
134
+ * 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.
132
137
  */
133
138
  const HOST_RULES = [
134
139
  '--dsw-menu-surface-fill', '--dsw-specific-menu', '--dsw-specific-sidebar-fill',
135
140
  '--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]',
141
+ '--dsw-alias-label-caption',
142
+ '.md-table-wide', '[data-dockkit-content]', '[data-dockkit-empty]',
137
143
  '[data-dockkit-float]', '[data-sidebar-right-open]', 'data-ds-dark-theme',
138
144
  ]
139
145
 
140
- function hostStyles(literals = HOST_RULES) {
141
- return [{
142
- ownerNode: { dataset: {} },
146
+ /** The one literal only THIS plugin's stylesheet declares (`.dab-card`, `side: 'own'`). */
147
+ const OWN_RULES = ['.dab-card']
148
+
149
+ /**
150
+ * A stylesheet the probe may read.
151
+ *
152
+ * `own: true` writes this plugin's marker — the ONLY thing that tells our sheets
153
+ * apart from the host's, since the host's own CSS modules set `data-plugin` on
154
+ * theirs, so a probe keying off that skips the entire host theme.
155
+ *
156
+ * The fixture exposes the attribute the way a REAL element does: `getAttribute`
157
+ * answers the dashed name, and `dataset` only the camel-cased key. Modelling this
158
+ * wrong is how a probe that reads `dataset['data-dab-side']` passes here and
159
+ * silently fails in a browser.
160
+ */
161
+ function sheet(literals, own = false) {
162
+ const attrs = {}
163
+ const dataset = {}
164
+ if (own) {
165
+ attrs[OWN_SHEET_ATTR] = 'own'
166
+ dataset[OWN_SHEET_DATASET] = 'own'
167
+ }
168
+ return {
169
+ ownerNode: {
170
+ dataset,
171
+ getAttribute: name => (name in attrs ? attrs[name] : null),
172
+ setAttribute: (name, value) => { attrs[name] = value },
173
+ },
143
174
  cssRules: literals.map(l => ({ cssText: `.x{color:red}/* ${l} */` })),
144
- }]
175
+ }
176
+ }
177
+
178
+ /** The host's sheets plus our own, minus whatever a test removes. */
179
+ function hostStyles(literals = HOST_RULES, ownLiterals = OWN_RULES) {
180
+ const sheets = [sheet(literals)]
181
+ if (ownLiterals.length > 0) sheets.push(sheet(ownLiterals, true))
182
+ return sheets
145
183
  }
146
184
 
147
185
  /**
@@ -192,6 +230,36 @@ test('a renamed menu token fails exactly that contract, and names the symptom',
192
230
  assert.match(row.symptom, /solid band|white band|sticky/i)
193
231
  })
194
232
 
233
+ test('a renamed token the host still aliases is caught by the rule half', () => {
234
+ // 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
237
+ // tell "renamed" from "still there".
238
+ const env = hostEnv({
239
+ ctx: healthyCtx(),
240
+ rules: HOST_RULES.filter(l => l !== '--dsw-menu-surface-fill'),
241
+ })
242
+ const results = probeContracts(env, 'en')
243
+ const row = results.find(r => r.id === 'token.menuSurface')
244
+ assert.equal(row.status, 'fail')
245
+ // The computed half would have passed: the token still resolves a value.
246
+ assert.match(row.detail, /host stylesheet includes/)
247
+ })
248
+
249
+ test('a host that stops publishing the palette fails the computed half', () => {
250
+ // 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
252
+ // the probe read the wrong element.
253
+ const env = hostEnv({
254
+ ctx: healthyCtx(),
255
+ tokens: PRESENT_TOKENS.filter(t => t !== '--dsw-alias-bg-base'),
256
+ })
257
+ const results = probeContracts(env, 'en')
258
+ const row = results.find(r => r.id === 'token.bgBase')
259
+ assert.equal(row.status, 'fail')
260
+ assert.match(row.reason, /--dsw-alias-bg-base/)
261
+ })
262
+
195
263
  test('a dropped session service fails the model hop', () => {
196
264
  const env = hostEnv({ ctx: healthyCtx({ sessions: undefined }) })
197
265
  const results = probeContracts(env, 'en')
@@ -200,19 +268,40 @@ test('a dropped session service fails the model hop', () => {
200
268
  })
201
269
 
202
270
  test('the plugin\'s own token stylesheet cannot make a host check pass', () => {
271
+ // The situation that produced the 0.1.7 white band: the plugin re-emits the
272
+ // 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
274
+ // by construction on exactly the host where the token was renamed away.
203
275
  const env = hostEnv({
204
276
  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.
277
+ // The host sheet is readable but has moved on: it no longer declares the
278
+ // menu fill under this name.
207
279
  rules: [],
208
280
  })
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
- }]
281
+ env.styleSheets = () => [
282
+ sheet([]), // the host's own, readable, declaring nothing
283
+ sheet(['--dsw-menu-surface-fill', '--dsw-specific-menu', '.dab-card'], true),
284
+ ]
285
+ const results = probeContracts(env, 'en')
286
+ const byId = id => results.find(r => r.id === id)
287
+ assert.equal(byId('token.menuSurface').status, 'fail')
288
+ assert.match(byId('token.menuSurface').reason, /no host stylesheet declares/)
289
+ assert.equal(byId('token.menuAlias').status, 'fail')
290
+ // …while the contract whose other half IS our own sheet still passes: this is
291
+ // what `side: 'own'` is for, and why the two sides must never be merged.
292
+ assert.equal(byId('settings.card').status, 'pass')
293
+ })
294
+
295
+ test('a host sheet carrying data-plugin is not mistaken for ours', () => {
296
+ // Regression for the probe bug that reported 7 healthy tokens as renamed: the
297
+ // host's CSS modules ALSO set `data-plugin` on their <style> elements, so a
298
+ // probe that skips "sheets with data-plugin" skips the host's whole theme.
299
+ const hostSheet = sheet(['--dsw-alias-bg-base'])
300
+ hostSheet.ownerNode.dataset.plugin = '@deepseek-ai/dsh-client-ui-theme'
301
+ const env = hostEnv({ ctx: healthyCtx(), rules: [] })
302
+ env.styleSheets = () => [hostSheet]
213
303
  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')
304
+ assert.equal(results.find(r => r.id === 'token.bgBase').status, 'pass')
216
305
  })
217
306
 
218
307
  test('a surface that is merely off screen is n/a, not a failure', () => {