dsh-background-by-model 0.4.1 → 0.4.2
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 +28 -1
- package/README.zh.md +28 -1
- package/lib/client.js +1652 -2
- package/lib/client.js.map +1 -1
- package/lib/host-scan-BDh0upIQ.js +1160 -0
- package/lib/host-scan.js +2 -0
- package/lib/index.js +24 -0
- package/lib/testing.js +560 -0
- package/package.json +128 -122
- package/scripts/host-contract-scan.mjs +182 -0
- package/test/host-check.test.mjs +296 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host self-check's own tests — run with
|
|
3
|
+
*
|
|
4
|
+
* pnpm test
|
|
5
|
+
*
|
|
6
|
+
* Three things are covered, and they are the three that can rot silently:
|
|
7
|
+
*
|
|
8
|
+
* 1. **The contract table holds together** — unique ids, machine-checkable
|
|
9
|
+
* sources, a known criticality, and at least one check or an explicit
|
|
10
|
+
* "documentation only" marker. A contract point added without a source would
|
|
11
|
+
* otherwise be invisible to the offline scan.
|
|
12
|
+
* 2. **The probe reports what is actually there** — the whole probe is driven
|
|
13
|
+
* against a fixture written in the shape of a host DOM, so "the host renamed
|
|
14
|
+
* `--dsw-specific-menu`" produces exactly the failure the panel claims, and a
|
|
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-*`
|
|
17
|
+
* class a component uses is defined in it, and the floor comparison is exact
|
|
18
|
+
* on the release triple.
|
|
19
|
+
*
|
|
20
|
+
* The tests run against the BUILT node entries (`lib/testing.js`), not the
|
|
21
|
+
* TypeScript sources: `node --test` cannot resolve extensionless relative imports
|
|
22
|
+
* inside a `.ts` file, and asserting on the shipped bundle is the more honest
|
|
23
|
+
* target anyway. `pnpm test` bundles first.
|
|
24
|
+
*/
|
|
25
|
+
import { test } from 'node:test'
|
|
26
|
+
import assert from 'node:assert/strict'
|
|
27
|
+
import { dirname, join, resolve } from 'node:path'
|
|
28
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
29
|
+
|
|
30
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
31
|
+
const pluginDir = resolve(here, '..')
|
|
32
|
+
const testingPath = join(pluginDir, 'lib', 'testing.js')
|
|
33
|
+
|
|
34
|
+
const { HOST_CONTRACTS, DSH_FLOOR, probeContracts, verdictOf, meetsFloor, UI_CSS } =
|
|
35
|
+
await import(pathToFileURL(testingPath).href)
|
|
36
|
+
|
|
37
|
+
const { readFile, readdir } = await import('node:fs/promises')
|
|
38
|
+
|
|
39
|
+
// ── 1. the contract table ────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
test('contract ids are unique and non-empty', () => {
|
|
42
|
+
const seen = new Set()
|
|
43
|
+
for (const c of HOST_CONTRACTS) {
|
|
44
|
+
assert.ok(c.id.length > 0, 'a contract has an empty id')
|
|
45
|
+
assert.ok(!seen.has(c.id), `duplicate contract id ${c.id}`)
|
|
46
|
+
seen.add(c.id)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('every contract is machine-checkable', () => {
|
|
51
|
+
for (const c of HOST_CONTRACTS) {
|
|
52
|
+
assert.ok(c.target.length > 0, `${c.id} has no target`)
|
|
53
|
+
assert.ok(c.sources.length > 0, `${c.id} declares no source, so the offline scan cannot see it`)
|
|
54
|
+
assert.ok(c.usedBy.length > 0, `${c.id} does not say which plugin file reads it`)
|
|
55
|
+
assert.ok(['core', 'looks', 'label'].includes(c.criticality), `${c.id} has an unknown criticality`)
|
|
56
|
+
assert.ok(['<=0.1.6', '0.1.7'].includes(c.since), `${c.id} has an unknown "since"`)
|
|
57
|
+
for (const s of c.sources) {
|
|
58
|
+
assert.ok(s.literal.length > 0, `${c.id} has a source without a literal`)
|
|
59
|
+
assert.ok(!s.path.includes('\\'), `${c.id} source path must use forward slashes: ${s.path}`)
|
|
60
|
+
if (s.path === '.') assert.equal(s.root, 'frontend', `${c.id}: "." only makes sense for the frontend root`)
|
|
61
|
+
}
|
|
62
|
+
if (c.checks.length === 0) {
|
|
63
|
+
// Documentation-only entries are allowed (a legacy alias we still accept),
|
|
64
|
+
// but they must be the minority and must not claim to be checkable.
|
|
65
|
+
assert.ok(c.criticality !== 'core', `${c.id} is critical but has no check`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('critical contracts carry a check that can fail', () => {
|
|
71
|
+
const critical = HOST_CONTRACTS.filter(c => c.criticality === 'core')
|
|
72
|
+
assert.ok(critical.length >= 4, 'the model hop and the menu split should be critical')
|
|
73
|
+
for (const c of critical) {
|
|
74
|
+
assert.ok(c.checks.length > 0, `${c.id} is critical but nothing verifies it at runtime`)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// ── 2. the probe, against a fixture in the shape of a host DOM ───────────────
|
|
79
|
+
|
|
80
|
+
/** A DOM stand-in with just enough selector support for the contract checks. */
|
|
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
|
+
}
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
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.
|
|
97
|
+
computedValue: (token, _on) => (host.get(`computed:${token}`) ?? ''),
|
|
98
|
+
styleSheets: () => [],
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Selectors for the surfaces that are on screen in every fixture. */
|
|
103
|
+
const CORE_PRESENT = [
|
|
104
|
+
'div[data-sidebar-right-panel]',
|
|
105
|
+
'div[role="dialog"][aria-modal="true"][aria-labelledby]',
|
|
106
|
+
'[data-shell-overlay]',
|
|
107
|
+
'[data-composer-card]',
|
|
108
|
+
'[data-cordis-panel]',
|
|
109
|
+
'[data-chat-flow]',
|
|
110
|
+
'[data-conversation-scroll]',
|
|
111
|
+
'[data-conversation-composer-overlay]',
|
|
112
|
+
'.dab-root',
|
|
113
|
+
'[data-dockkit-strip]',
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
/** Every design token the contracts read; each one resolves in a healthy host. */
|
|
117
|
+
const PRESENT_TOKENS = [
|
|
118
|
+
'--dsw-alias-bg-base',
|
|
119
|
+
'--dsw-alias-bg-layer-1',
|
|
120
|
+
'--dsw-alias-bg-layer-2',
|
|
121
|
+
'--dsw-alias-bg-layer-3',
|
|
122
|
+
'--dsw-specific-menu',
|
|
123
|
+
'--dsw-menu-surface-fill',
|
|
124
|
+
'--dsw-specific-sidebar-fill',
|
|
125
|
+
'--dsw-specific-input-major',
|
|
126
|
+
'--dsw-alias-label-caption',
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
/**
|
|
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.
|
|
132
|
+
*/
|
|
133
|
+
const HOST_RULES = [
|
|
134
|
+
'--dsw-menu-surface-fill', '--dsw-specific-menu', '--dsw-specific-sidebar-fill',
|
|
135
|
+
'--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]',
|
|
137
|
+
'[data-dockkit-float]', '[data-sidebar-right-open]', 'data-ds-dark-theme',
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
function hostStyles(literals = HOST_RULES) {
|
|
141
|
+
return [{
|
|
142
|
+
ownerNode: { dataset: {} },
|
|
143
|
+
cssRules: literals.map(l => ({ cssText: `.x{color:red}/* ${l} */` })),
|
|
144
|
+
}]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* A fixture host: `selectors` are the ones that match, `rules` the literals the
|
|
149
|
+
* host stylesheet declares, and every token in `PRESENT_TOKENS` resolves unless
|
|
150
|
+
* the caller drops it.
|
|
151
|
+
*/
|
|
152
|
+
function hostEnv({ ctx, selectors = CORE_PRESENT, rules = HOST_RULES, tokens = PRESENT_TOKENS }) {
|
|
153
|
+
// A Map, not a Set: the fixture must answer with the token's VALUE, and only a
|
|
154
|
+
// Map carries one (a Set has no `get`).
|
|
155
|
+
const host = new Map()
|
|
156
|
+
for (const s of selectors) host.set(s, true)
|
|
157
|
+
for (const t of tokens) host.set(`computed:${t}`, '#123456')
|
|
158
|
+
return { ...makeDom(host), styleSheets: () => hostStyles(rules), ctx }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The healthy fixture: nothing missing, every service published. */
|
|
162
|
+
const healthyEnv = () => hostEnv({ ctx: healthyCtx() })
|
|
163
|
+
|
|
164
|
+
/** A context publishing the four services the model hop needs. */
|
|
165
|
+
function healthyCtx(overrides = {}) {
|
|
166
|
+
const face = { subscribe: () => () => {}, getSnapshot: () => ({}) }
|
|
167
|
+
const services = {
|
|
168
|
+
sessions: { list: face, binding: () => ({}) },
|
|
169
|
+
uiSession: { current: face },
|
|
170
|
+
modelDirectories: { directoryFor: () => ({ store: face }) },
|
|
171
|
+
...overrides,
|
|
172
|
+
}
|
|
173
|
+
return { get: id => services[id] }
|
|
174
|
+
}
|
|
175
|
+
|
|
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')
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('a renamed menu token fails exactly that contract, and names the symptom', () => {
|
|
184
|
+
// The 0.1.8 rename: the host stops declaring the token the sticky group
|
|
185
|
+
// headings paint from. Everything else still answers.
|
|
186
|
+
const env = hostEnv({ ctx: healthyCtx(), rules: HOST_RULES.filter(l => l !== '--dsw-menu-surface-fill') })
|
|
187
|
+
const results = probeContracts(env, 'en')
|
|
188
|
+
const failed = results.filter(r => r.status === 'fail').map(r => r.id)
|
|
189
|
+
assert.deepEqual(failed, ['token.menuSurface'])
|
|
190
|
+
const row = results.find(r => r.id === 'token.menuSurface')
|
|
191
|
+
assert.match(row.reason, /--dsw-menu-surface-fill/)
|
|
192
|
+
assert.match(row.symptom, /solid band|white band|sticky/i)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('a dropped session service fails the model hop', () => {
|
|
196
|
+
const env = hostEnv({ ctx: healthyCtx({ sessions: undefined }) })
|
|
197
|
+
const results = probeContracts(env, 'en')
|
|
198
|
+
assert.equal(results.find(r => r.id === 'sessions.list').status, 'fail')
|
|
199
|
+
assert.equal(results.find(r => r.id === 'sessions.binding.projections').status, 'skip')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('the plugin\'s own token stylesheet cannot make a host check pass', () => {
|
|
203
|
+
const env = hostEnv({
|
|
204
|
+
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.
|
|
207
|
+
rules: [],
|
|
208
|
+
})
|
|
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
|
+
}]
|
|
213
|
+
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')
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test('a surface that is merely off screen is n/a, not a failure', () => {
|
|
219
|
+
// 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,
|
|
221
|
+
// the right panel and the Cordis panel are not. Nothing is broken; those
|
|
222
|
+
// surfaces simply are not there to observe.
|
|
223
|
+
const env = hostEnv({
|
|
224
|
+
ctx: healthyCtx(),
|
|
225
|
+
selectors: ['.dab-root', 'div[role="dialog"][aria-modal="true"][aria-labelledby]', '[data-shell-overlay]'],
|
|
226
|
+
})
|
|
227
|
+
const results = probeContracts(env, 'en')
|
|
228
|
+
const byId = id => results.find(r => r.id === id)
|
|
229
|
+
assert.equal(byId('chat.flow').status, 'skip')
|
|
230
|
+
assert.equal(byId('conversation.scroll').status, 'skip')
|
|
231
|
+
assert.equal(byId('trajectory.root').status, 'skip')
|
|
232
|
+
assert.equal(byId('rightbar.panel').status, 'skip')
|
|
233
|
+
assert.equal(byId('cordis.panel').status, 'skip')
|
|
234
|
+
assert.equal(byId('composer.card').status, 'skip')
|
|
235
|
+
assert.equal(byId('settings.dialog').status, 'pass')
|
|
236
|
+
assert.deepEqual(results.filter(r => r.status === 'fail').map(r => r.id), [])
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
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
|
|
241
|
+
// dialog's own selector still matches. A surface whose anchor IS on screen and
|
|
242
|
+
// whose selector is not is a rename, and must not read as "not observable".
|
|
243
|
+
const env = hostEnv({
|
|
244
|
+
ctx: healthyCtx(),
|
|
245
|
+
// The dock chrome is mounted (the panel container exists) but the panel
|
|
246
|
+
// attribute the contract names is gone.
|
|
247
|
+
selectors: ['.dab-root', '[data-dockkit-strip]'],
|
|
248
|
+
})
|
|
249
|
+
const results = probeContracts(env, 'en')
|
|
250
|
+
const byId = id => results.find(r => r.id === id)
|
|
251
|
+
assert.equal(byId('rightbar.panel').status, 'fail')
|
|
252
|
+
assert.match(byId('rightbar.panel').reason, /data-sidebar-right-panel/)
|
|
253
|
+
assert.equal(byId('chat.flow').status, 'skip')
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
// ── 3. the settings UI's own stylesheet ──────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
test('every dab-* class a component uses is defined in the UI stylesheet', async () => {
|
|
259
|
+
const dir = join(pluginDir, 'src', 'client')
|
|
260
|
+
const files = []
|
|
261
|
+
const walk = async d => {
|
|
262
|
+
for (const entry of await readdir(d, { withFileTypes: true })) {
|
|
263
|
+
const p = join(d, entry.name)
|
|
264
|
+
if (entry.isDirectory()) await walk(p)
|
|
265
|
+
else if (entry.name.endsWith('.tsx')) files.push(p)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
await walk(dir)
|
|
269
|
+
assert.ok(files.length > 0, 'no component sources found')
|
|
270
|
+
|
|
271
|
+
const missing = []
|
|
272
|
+
for (const file of files) {
|
|
273
|
+
const text = await readFile(file, 'utf8')
|
|
274
|
+
for (const m of text.matchAll(/(?<![\w-])dab-[a-z0-9-]+/g)) {
|
|
275
|
+
const cls = m[0]
|
|
276
|
+
// `--dab-mono` and friends are this plugin's own CSS VARIABLES, not classes.
|
|
277
|
+
if (text.slice(Math.max(0, m.index - 2), m.index) === '--') continue
|
|
278
|
+
if (UI_CSS.includes(`.${cls}`)) continue
|
|
279
|
+
if (missing.some(x => x.cls === cls && x.file === file)) continue
|
|
280
|
+
missing.push({ cls, file: file.slice(pluginDir.length + 1) })
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
assert.deepEqual(missing, [], `classes used but never styled: ${missing.map(m => `${m.cls} (${m.file})`).join(', ')}`)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
// ── 4. the floor comparison ──────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
test('the floor comparison is coarse on purpose and exact on the triple', () => {
|
|
289
|
+
assert.equal(meetsFloor('0.1.7-rc.2', DSH_FLOOR), true, 'the floor itself satisfies the floor')
|
|
290
|
+
assert.equal(meetsFloor('0.1.7', DSH_FLOOR), true)
|
|
291
|
+
assert.equal(meetsFloor('0.2.0', DSH_FLOOR), true)
|
|
292
|
+
assert.equal(meetsFloor('1.0.0', DSH_FLOOR), true)
|
|
293
|
+
assert.equal(meetsFloor('0.1.6', DSH_FLOOR), false)
|
|
294
|
+
assert.equal(meetsFloor('0.1.0', DSH_FLOOR), false)
|
|
295
|
+
assert.equal(meetsFloor('not-a-version', DSH_FLOOR), null)
|
|
296
|
+
})
|