dsh-scout 0.9.0 → 0.9.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.
@@ -0,0 +1,246 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs'
4
+ import { dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ /**
8
+ * Distribution-contract tests for dsh-scout.
9
+ *
10
+ * These assert the fields the DSH plugin manager actually reads, not just the
11
+ * fields we happen to publish:
12
+ * - `engines.dsh` / `dsh.manifestVersion` are the author declarations described
13
+ * by `@deepseek-ai/dsh-package-manifest` (`engines` is "declarative until a
14
+ * reader enforces it"; the enforced gate is the `@deepseek-ai/dsh-tools`
15
+ * peer range, so the two must stay aligned).
16
+ * - `readPluginMeta()` reads `${name}/locale/en.json` and every sibling
17
+ * `<lang>.json` through the Node ESM resolver. A locale file that is present
18
+ * on disk but absent from `exports` fails with ERR_PACKAGE_PATH_NOT_EXPORTED,
19
+ * which DSH treats as "absent" - and because `dictionariesOf()` resolves
20
+ * every `.json` in the directory, one unexported dictionary costs the plugin
21
+ * *all* of its display metadata.
22
+ * - `iconOf()` accepts a manifest-relative SVG/PNG/JPEG/WebP of at most
23
+ * 256 KiB that stays inside the package directory after realpath resolution.
24
+ *
25
+ * This suite reads package.json, locale/, and assets/ only. It never imports
26
+ * `dist/`, so it is independent of the build.
27
+ */
28
+
29
+ const testsDir = dirname(fileURLToPath(import.meta.url))
30
+ const packageRoot = resolve(testsDir, '..')
31
+ const manifestPath = join(packageRoot, 'package.json')
32
+ const localeDir = join(packageRoot, 'locale')
33
+
34
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
35
+
36
+ const PACKAGE_NAME = 'dsh-scout'
37
+ /**
38
+ * Release checklist anchor: this literal is asserted against `package.json`,
39
+ * so it must be bumped by hand with every release. Do not read it back from the
40
+ * manifest - a tautological assertion would stop catching a forgotten bump.
41
+ */
42
+ const RELEASE_VERSION = '0.9.2'
43
+ const DSH_RANGE = '>=0.1.0-rc.6 <0.3.0-0'
44
+ const NODE_RANGE = '>=22.19.0'
45
+ const MAX_ICON_BYTES = 256 * 1024
46
+ const ICON_EXTENSIONS = new Set(['.svg', '.png', '.jpg', '.jpeg', '.webp'])
47
+ /** Mirrors the language-id rule DSH applies to locale filenames. */
48
+ const LANGUAGE_ID = /^[a-z]{2,8}(?:-[a-z0-9]{1,8})*$/
49
+ const GLOB_CHARS = /[*?[\]{}]/
50
+
51
+ const localeNames = readdirSync(localeDir)
52
+ .filter((name) => name.endsWith('.json'))
53
+ .sort()
54
+
55
+ const localeEntries = (name) => `./locale/${name}`
56
+
57
+ const slug = (value) => value.replace(/^\.\//, '').replace(/\/+$/, '')
58
+
59
+ test(`publishes the ${RELEASE_VERSION} package identity`, () => {
60
+ assert.equal(manifest.name, PACKAGE_NAME)
61
+ assert.equal(manifest.version, RELEASE_VERSION)
62
+ assert.equal(manifest.type, 'module')
63
+ assert.equal(manifest.license, 'MIT')
64
+ assert.equal(typeof manifest.description, 'string')
65
+ assert.ok(manifest.description.trim().length > 0, 'description must not be blank')
66
+ })
67
+
68
+ test('declares the DSH compatibility range under engines', () => {
69
+ assert.equal(manifest.engines.node, NODE_RANGE)
70
+ // DSH_RANGE is deliberately spelled with a prerelease-aware upper bound.
71
+ // Under default node-semver rules `0.2.0-rc.2` does NOT satisfy
72
+ // `>=0.1.0-rc.6 <0.3.0-0` - a prerelease only matches when a comparator
73
+ // carries one - so this range admits the 0.2 prerelease line only because
74
+ // DSH evaluates it with `{ includePrerelease: true }`. Do not "simplify" it
75
+ // to an exact version or drop the `-0` suffix: the failure mode is a
76
+ // silently disabled plugin row (`dsh: disabling profile plugin ...`), not an
77
+ // error, and `peerDependencies` gives no second chance.
78
+ assert.equal(manifest.engines.dsh, DSH_RANGE)
79
+ })
80
+
81
+ test('keeps the declared DSH range aligned with the range DSH enforces', () => {
82
+ // evaluatePluginCompatibility() only inspects @deepseek-ai/dsh / dsh-* peers,
83
+ // so the tool peer is what actually enables or disables this plugin.
84
+ assert.equal(manifest.peerDependencies['@deepseek-ai/dsh-tools'], DSH_RANGE)
85
+ assert.equal(manifest.peerDependencies['@deepseek-ai/cordis'], '>=4.0.2 <5')
86
+ // The validator is a runtime dependency, so it is pinned to the same minor
87
+ // line the DSH host series itself resolves (`~3.18.4`). A caret range would
88
+ // admit 3.19.x and let the plugin validate Config objects with a different
89
+ // build than the host that hands them over. Config identity survives that
90
+ // (recognition goes through the global `Symbol.for('schemastery')`), so the
91
+ // damage would be silent behavioural drift rather than a type error - which
92
+ // is exactly why the range is asserted here instead of left to review.
93
+ assert.equal(manifest.dependencies['@deepseek-ai/schemastery'], '~3.18.4')
94
+ assert.ok(
95
+ manifest.dependencies['@deepseek-ai/schemastery'].startsWith('~'),
96
+ 'the schemastery range must stay minor-locked to the host series, not float on a caret',
97
+ )
98
+ })
99
+
100
+ test('keeps dsh.manifestVersion alongside the existing bundle patch', () => {
101
+ assert.equal(manifest.dsh.manifestVersion, 1)
102
+ assert.equal(manifest.dsh.bundle.patch, './cordis.patch.yml')
103
+ assert.ok(
104
+ statSync(resolve(packageRoot, manifest.dsh.bundle.patch)).isFile(),
105
+ 'the declared bundle patch must exist',
106
+ )
107
+ })
108
+
109
+ test('exports every locale dictionary through the package exports map', () => {
110
+ assert.ok(localeNames.includes('en.json'), 'locale/en.json is the English fallback anchor')
111
+ for (const name of localeNames) {
112
+ const entry = localeEntries(name)
113
+ assert.equal(
114
+ manifest.exports[entry],
115
+ entry,
116
+ `${entry} must be exported: DSH resolves locales through the ESM resolver and ` +
117
+ 'silently treats ERR_PACKAGE_PATH_NOT_EXPORTED as "no metadata"',
118
+ )
119
+ assert.ok(
120
+ statSync(resolve(packageRoot, entry)).isFile(),
121
+ `${entry} must point at a real file`,
122
+ )
123
+ }
124
+ })
125
+
126
+ test('names every dictionary with a valid, unique language id', () => {
127
+ const seen = new Set()
128
+ for (const name of localeNames) {
129
+ const id = name.slice(0, -'.json'.length)
130
+ assert.match(id, LANGUAGE_ID, `${name} must be named after a language id`)
131
+ assert.equal(id, id.toLowerCase(), `${name} must use a lowercase language id`)
132
+ assert.ok(!seen.has(id), `duplicate locale ${id}`)
133
+ seen.add(id)
134
+ }
135
+ assert.ok(seen.has('zh'), 'a Chinese dictionary is expected for the zh-CN audience')
136
+ })
137
+
138
+ test('resolves every locale resource the way the plugin manager does', () => {
139
+ // Self-reference resolution goes through package.json "exports" exactly like
140
+ // resolvePluginResource(`${specifier}/locale/${file}`) does in production.
141
+ for (const name of localeNames) {
142
+ const specifier = `${PACKAGE_NAME}/locale/${name}`
143
+ let url
144
+ try {
145
+ url = import.meta.resolve(specifier)
146
+ } catch (error) {
147
+ assert.fail(
148
+ `${specifier} does not resolve (${error.code ?? error.message}); DSH would drop ` +
149
+ 'every display field for this plugin',
150
+ )
151
+ }
152
+ assert.equal(fileURLToPath(url), join(localeDir, name))
153
+ }
154
+ })
155
+
156
+ test('publishes non-empty localized titles and descriptions', () => {
157
+ for (const name of localeNames) {
158
+ const dictionary = JSON.parse(readFileSync(join(localeDir, name), 'utf8'))
159
+ assert.equal(
160
+ typeof dictionary.meta,
161
+ 'object',
162
+ `${name}: DSH reads meta.title / meta.description and rejects non-object meta`,
163
+ )
164
+ assert.ok(dictionary.meta !== null && !Array.isArray(dictionary.meta), `${name}: meta must be an object`)
165
+ for (const field of ['title', 'description']) {
166
+ const value = dictionary.meta[field]
167
+ assert.equal(typeof value, 'string', `${name}: meta.${field} must be a string`)
168
+ assert.ok(value.trim().length > 0, `${name}: meta.${field} must not be blank`)
169
+ }
170
+ // Display hygiene: the plugin manager renders these in a single row.
171
+ assert.ok(dictionary.meta.title.length <= 60, `${name}: meta.title is too long for a list row`)
172
+ assert.ok(dictionary.meta.description.length <= 240, `${name}: meta.description is too long`)
173
+ }
174
+ })
175
+
176
+ test('declares an icon that satisfies the reader limits', () => {
177
+ const icon = manifest.icon
178
+ assert.equal(typeof icon, 'string', 'package.json must declare an icon')
179
+ assert.ok(icon.trim().length > 0)
180
+ assert.ok(!isAbsolute(icon), 'icon must be a relative path')
181
+ assert.ok(
182
+ !/^[A-Za-z][A-Za-z\d+.-]*:/u.test(icon),
183
+ 'icon must be a file path, not a URL specifier',
184
+ )
185
+ assert.ok(
186
+ ICON_EXTENSIONS.has(extname(icon).toLowerCase()),
187
+ `icon must be SVG, PNG, JPEG, or WebP, got ${extname(icon)}`,
188
+ )
189
+
190
+ const root = realpathSync(packageRoot)
191
+ const file = realpathSync(resolve(packageRoot, icon))
192
+ const local = relative(root, file)
193
+ assert.ok(
194
+ local !== '..' && !local.startsWith(`..${sep}`) && !isAbsolute(local),
195
+ 'icon must remain inside the package directory after realpath resolution',
196
+ )
197
+ const stat = statSync(file)
198
+ assert.ok(stat.isFile(), 'icon must be a regular file')
199
+ assert.ok(stat.size <= MAX_ICON_BYTES, `icon must be at most 256 KiB, got ${stat.size} bytes`)
200
+ assert.equal(readFileSync(file).length, stat.size)
201
+ })
202
+
203
+ test('ships a standalone SVG icon safe to embed as a data URL', () => {
204
+ const iconPath = resolve(packageRoot, manifest.icon)
205
+ assert.equal(extname(manifest.icon).toLowerCase(), '.svg')
206
+ const bytes = readFileSync(iconPath)
207
+ const text = bytes.toString('utf8')
208
+ assert.ok(!text.startsWith('\uFEFF'), 'the icon must not start with a byte-order mark')
209
+ assert.match(text.trimStart(), /^<svg[\s>]/, 'the icon root element must be <svg>')
210
+ assert.ok(
211
+ text.includes('xmlns="http://www.w3.org/2000/svg"'),
212
+ 'a standalone SVG needs the namespace so <img> renders it',
213
+ )
214
+ assert.match(text, /viewBox="0 0 \d+ \d+"/, 'the icon must scale via viewBox')
215
+ assert.match(text, /<\/svg>\s*$/, 'the icon must close its root element')
216
+ // DSH reads the file into a data: URL and renders it as an image, so it must
217
+ // carry no scripting and no external references.
218
+ assert.ok(!/<\s*script/i.test(text), 'the icon must not contain scripting')
219
+ assert.ok(
220
+ !/(?:href|src)\s*=\s*(?:"|')?(?:https?:)?\/\//i.test(text),
221
+ 'the icon must not reference external resources',
222
+ )
223
+ })
224
+
225
+ test('lists locale, assets, and this suite in the published file allowlist', () => {
226
+ assert.ok(Array.isArray(manifest.files))
227
+ for (const entry of ['dist', 'cordis.patch.yml', 'locale', 'assets', 'tests/package.test.mjs']) {
228
+ assert.ok(manifest.files.includes(entry), `files must include ${entry}`)
229
+ }
230
+ for (const entry of manifest.files) {
231
+ if (GLOB_CHARS.test(entry)) continue
232
+ assert.ok(existsSync(resolve(packageRoot, entry)), `files entry ${entry} does not exist`)
233
+ }
234
+ })
235
+
236
+ test('covers every reader-visible resource with the files allowlist', () => {
237
+ const entries = manifest.files.map(slug)
238
+ const covered = (relPath) =>
239
+ entries.some((entry) => relPath === entry || relPath.startsWith(`${entry}/`))
240
+
241
+ assert.ok(covered(slug(manifest.dsh.bundle.patch)), 'the bundle patch must be published')
242
+ assert.ok(covered(slug(manifest.icon)), 'the icon must be published')
243
+ for (const name of localeNames) {
244
+ assert.ok(covered(`locale/${name}`), `locale/${name} must be published`)
245
+ }
246
+ })