mirror of
https://github.com/zhu1090093659/dsh-web.git
synced 2026-09-28 14:24:03 +08:00
Move every family manifest, the plugin scaffold and the shared workspace package onto the 0.1.7-rc.2 cohort, together with the surfaces that state the same fact: - the dsh.engines.dsh floors and the matching @deepseek-ai/dsh host peers - the release-age exclusion ledger and the two packageExtensions keys - the root README badges, the CI/release mount-smoke pins and the docs/publish-prep.md and docs/plugins.md prose Two rc.2 facts were verified against the published artifacts rather than inferred from the version number: - the shell's frozen static module table is unchanged (the same nine specifiers in the dsh-web-frontend rc.2 dist bundle), so shared/web-platform.ts keeps its list and only its provenance comment moves; the new dsh-client-shortcuts is an ordinary client plugin, not a static module - @deepseek-ai/dsh-client-ui-primitives@0.1.7-rc.2 imports a further undeclared face, @deepseek-ai/dsh-util-code-language, so the primitives packageExtensions patch gains that pin The satellite peer floors in pnpm-lock.yaml still read >=0.1.7-rc.1: they belong to the satellite packages and move when those repositories release the aligned version.
214 lines
11 KiB
JavaScript
214 lines
11 KiB
JavaScript
/**
|
|
* Aggregate patch invariants: every INSERT row id is web-ui-* namespaced and
|
|
* unique within one aggregate, and no aggregate id collides with any
|
|
* standalone package's own row id (the coexistence guarantee). The generated
|
|
* files are the contract — scripts/aggregate.mjs --check enforces drift
|
|
* separately.
|
|
*/
|
|
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
import { dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import assert from 'node:assert/strict'
|
|
import { test } from 'node:test'
|
|
|
|
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
|
|
const ROOT = join(SCRIPT_DIR, '..')
|
|
|
|
/** Parse the INSERT row ids of one cordis.patch.yml (rows are indented under
|
|
* an `- insert:` block; top-level `- id:` entries are excluded). */
|
|
function idsOf(relPath) {
|
|
const lines = readFileSync(join(ROOT, relPath), 'utf8').split(/\r?\n/)
|
|
return lines
|
|
.filter((line) => /^ {4}- id: /.test(line))
|
|
.map((line) => line.trim().replace(/^- id: /, ''))
|
|
}
|
|
|
|
const AGGREGATES = ['packages/dsh-web-all/cordis.patch.yml']
|
|
|
|
test('aggregate rows are web-ui-* namespaced and unique', () => {
|
|
for (const rel of AGGREGATES) {
|
|
const ids = idsOf(rel)
|
|
assert.ok(ids.length > 0, `${rel} should carry rows`)
|
|
assert.equal(new Set(ids).size, ids.length, `${rel} ids must be unique`)
|
|
for (const id of ids) {
|
|
assert.match(id, /^web-ui-[a-z0-9-]+$/, `${rel} id must be namespaced: ${id}`)
|
|
}
|
|
}
|
|
})
|
|
|
|
test('aggregate ids never collide with standalone package ids', () => {
|
|
const aggregateIds = new Set(AGGREGATES.flatMap(idsOf))
|
|
const standalonePatches = []
|
|
for (const base of ['packages']) {
|
|
if (!existsSync(join(ROOT, base))) continue
|
|
for (const entry of readdirSync(join(ROOT, base), { withFileTypes: true })) {
|
|
if (!entry.isDirectory()) continue
|
|
const patch = join(base, entry.name, 'cordis.patch.yml')
|
|
const abs = join(ROOT, patch)
|
|
try {
|
|
readFileSync(abs)
|
|
} catch {
|
|
continue
|
|
}
|
|
const normPatch = patch.replaceAll('\\', '/')
|
|
if (normPatch === 'packages/dsh-web-all/cordis.patch.yml') continue
|
|
standalonePatches.push(patch)
|
|
}
|
|
}
|
|
assert.ok(standalonePatches.length > 10, 'expected to scan the standalone packages')
|
|
for (const patch of standalonePatches) {
|
|
for (const id of idsOf(patch)) {
|
|
assert.ok(!aggregateIds.has(id), `aggregate id "${id}" collides with standalone row in ${patch}`)
|
|
}
|
|
}
|
|
})
|
|
|
|
test('no aggregate deps entry resolves to a private workspace package', () => {
|
|
const aggregates = []
|
|
for (const base of ['packages']) {
|
|
if (!existsSync(join(ROOT, base))) continue
|
|
for (const entry of readdirSync(join(ROOT, base), { withFileTypes: true })) {
|
|
if (!entry.isDirectory()) continue
|
|
const yml = join(base, entry.name, 'aggregate.yml')
|
|
if (existsSync(join(ROOT, yml))) aggregates.push(yml)
|
|
}
|
|
}
|
|
assert.ok(aggregates.length >= 1, 'expected at least one aggregate manifest')
|
|
for (const yml of aggregates) {
|
|
let section = null
|
|
for (const raw of readFileSync(join(ROOT, yml), 'utf8').split(/\r?\n/)) {
|
|
const line = raw.trim()
|
|
if (!line || line.startsWith('#')) continue
|
|
const sectionMatch = line.match(/^[A-Za-z0-9_-]+:\s*$/)
|
|
if (sectionMatch) {
|
|
section = line.slice(0, -1)
|
|
continue
|
|
}
|
|
if (section !== 'deps' || !line.startsWith('- ')) continue
|
|
const entry = line.slice(2).trim().replace(/\s+#.*$/, '')
|
|
const pkgPath = join(ROOT, dirname(yml), entry, 'package.json')
|
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
assert.notEqual(pkg.private, true, 'aggregate deps entry is private: ' + yml + ' -> ' + entry)
|
|
}
|
|
}
|
|
})
|
|
|
|
test('web-ui-all leaves the unbundled dsh-better-sidebar out of the patch', () => {
|
|
const patch = readFileSync(join(ROOT, 'packages/dsh-web-all/cordis.patch.yml'), 'utf8')
|
|
// Alpha-branch decision (2026-09-17): the plugin's 0.19.1 peers declare
|
|
// ^0.1.5-rc.1, which does not cover this branch's 0.1.7-rc.2 cohort, so the
|
|
// aggregate neither mounts nor depends on it. Re-add the row in aggregate.yml
|
|
// together with this assertion when the branch bundles it again.
|
|
assert.doesNotMatch(patch, /^ {4}- id: web-ui-better-sidebar$/m, 'dsh-better-sidebar must not be a bundled row on the alpha branch')
|
|
assert.doesNotMatch(patch, /^ {6}name: 'dsh-better-sidebar'$/m)
|
|
})
|
|
|
|
test('web-ui-all does not mount the dsh-client-runtime-dependent @mlgbnb/dsh-archive-manager', () => {
|
|
const patch = readFileSync(join(ROOT, 'packages/dsh-web-all/cordis.patch.yml'), 'utf8')
|
|
// @mlgbnb/dsh-archive-manager imports the removed @deepseek-ai/dsh-client-runtime
|
|
// face, so on the alpha.2 cohort the host loader cannot resolve its entry and
|
|
// the whole boot fails. Its latest upstream build is still 1.0.7, so it stays
|
|
// excluded; re-add the row and this assertion together with package.json deps
|
|
// when upstream ships an alpha.2-compatible build.
|
|
assert.doesNotMatch(patch, /^ {4}- id: web-ui-archive-manager$/m, '@mlgbnb/dsh-archive-manager must not be mounted on the alpha.2 cohort')
|
|
})
|
|
|
|
test('web-ui-all keeps the removed doctor/describe-image plugins out but their tombstone exports alive', () => {
|
|
// 2026-09-23 product decision: dsh-doctor and dsh-tool-describe-image were
|
|
// removed from the family. No patch row or workspace dependency may name
|
|
// them again without an explicit re-adoption decision; their shell subpath
|
|
// exports stay as tombstones so a stale profile row fails soft instead of
|
|
// aborting the boot with ERR_PACKAGE_PATH_NOT_EXPORTED.
|
|
const patch = readFileSync(join(ROOT, 'packages/dsh-web-all/cordis.patch.yml'), 'utf8')
|
|
assert.doesNotMatch(patch, /web-ui-(doctor|describe-image)/, 'removed plugins must not reappear as patch rows')
|
|
const pkg = JSON.parse(readFileSync(join(ROOT, 'packages/dsh-web-all/package.json'), 'utf8'))
|
|
assert.ok(!('@linxin666/dsh-doctor' in pkg.dependencies), 'dsh-doctor must not be a workspace dependency')
|
|
assert.ok(!('@linxin666/dsh-tool-describe-image' in pkg.dependencies), 'dsh-tool-describe-image must not be a workspace dependency')
|
|
assert.equal(pkg.exports['./doctor'], './lib/shells/shell.js', 'doctor tombstone export must survive')
|
|
assert.equal(pkg.exports['./describe-image'], './lib/shells/shell.js', 'describe-image tombstone export must survive')
|
|
})
|
|
|
|
test('web-ui-all ships the opt-in family rows disabled by default', () => {
|
|
// The manifest's inactive list renders trailing bare "disabled: true"
|
|
// overrides; users opt in per row in the plugin manager (a user-layer
|
|
// "disabled: false" override wins over the bundle default).
|
|
const yml = readFileSync(join(ROOT, 'packages/dsh-web-all/aggregate.yml'), 'utf8')
|
|
let section = null
|
|
const inactive = []
|
|
for (const raw of yml.split(/\r?\n/)) {
|
|
const line = raw.trim()
|
|
if (!line || line.startsWith('#')) continue
|
|
const sectionMatch = line.match(/^[A-Za-z0-9_-]+:\s*$/)
|
|
if (sectionMatch) {
|
|
section = line.slice(0, -1)
|
|
continue
|
|
}
|
|
if (section === 'inactive' && line.startsWith('- ')) inactive.push(line.slice(2).trim())
|
|
}
|
|
assert.ok(inactive.length > 0, 'aggregate.yml should declare inactive opt-in rows')
|
|
const patch = readFileSync(join(ROOT, 'packages/dsh-web-all/cordis.patch.yml'), 'utf8')
|
|
for (const id of inactive) {
|
|
assert.match(patch, new RegExp('^- id: ' + id + '\\n disabled: true$', 'm'), 'inactive row missing its disabled override: ' + id)
|
|
}
|
|
})
|
|
|
|
test('family shell rows keep the real plugin config at the row config root', () => {
|
|
// The Host settings surface edits an entry's OWN Config schema, so a
|
|
// shell-wrapped family row must present the family plugin's fields where a
|
|
// standalone install of that package keeps them: beside the shell's own
|
|
// `plugin` key, never nested under a second `config:` key (which the
|
|
// settings surface cannot address, and which left a shelled row silently
|
|
// unconfigurable while its card showed an editable form).
|
|
const patch = readFileSync(join(ROOT, 'packages/dsh-web-all/cordis.patch.yml'), 'utf8')
|
|
const lines = patch.split(/\r?\n/)
|
|
let checked = 0
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const name = lines[i].match(/^ name: '@linxin666\/dsh-web-all\/(\S+)'$/)
|
|
if (!name) continue
|
|
assert.equal(lines[i + 1], ' config:', 'shell row ' + name[1] + ' must carry a config block')
|
|
assert.match(lines[i + 2] ?? '', /^ plugin: '@linxin666\/[^']+'$/, 'shell row ' + name[1] + ' must name the real plugin first')
|
|
for (let j = i + 3; j < lines.length && /^ {8}\S/.test(lines[j]); j++) {
|
|
assert.doesNotMatch(lines[j], /^ {8}config:/, 'shell row ' + name[1] + ' nests the family config one level too deep')
|
|
}
|
|
checked += 1
|
|
}
|
|
assert.ok(checked > 10, 'expected the generated patch to carry the family rows, found ' + checked)
|
|
})
|
|
|
|
test('web-ui-all declares no retire target the current cohort no longer mounts', () => {
|
|
// The official bundles alpha.2 ships mount no row this aggregate supersedes:
|
|
// the official archived-sessions page (`ui-settings-unarchive-sessions`) is
|
|
// gone from dsh-web-app, so a retire override for it changed nothing while
|
|
// making the loader warn `patch: entry ... not found` on every profile boot.
|
|
// `dsh-session-archive` owning the official `archived-sessions` section id
|
|
// (2026-09-15 native-first decision) stands on its own. Declaring a target
|
|
// again requires proving the foreign row exists in the SAME cohort with
|
|
// `dsh --profile <name> --dump-config`, then updating this test.
|
|
const yml = readFileSync(join(ROOT, 'packages/dsh-web-all/aggregate.yml'), 'utf8')
|
|
let section = null
|
|
const retire = []
|
|
for (const raw of yml.split(/\r?\n/)) {
|
|
const line = raw.trim()
|
|
if (!line || line.startsWith('#')) continue
|
|
const sectionMatch = line.match(/^[A-Za-z0-9_-]+:\s*$/)
|
|
if (sectionMatch) {
|
|
section = line.slice(0, -1)
|
|
continue
|
|
}
|
|
if (section === 'retire' && line.startsWith('- ')) retire.push(line.slice(2).trim())
|
|
}
|
|
assert.deepEqual(retire, [], 'retire targets must be verified against the installed cohort before being declared')
|
|
const patch = readFileSync(join(ROOT, 'packages/dsh-web-all/cordis.patch.yml'), 'utf8')
|
|
assert.doesNotMatch(patch, /^- id: ui-settings-unarchive-sessions$/m, 'the aggregate must not patch a foreign row no bundle mounts')
|
|
})
|
|
|
|
test('web-ui-all leaves the deprecated @morlay/better-session integration out', () => {
|
|
const patch = readFileSync(join(ROOT, 'packages/dsh-web-all/cordis.patch.yml'), 'utf8')
|
|
// The deprecated integration was removed from the aggregate; these rows must
|
|
// never come back without an explicit re-adoption decision (see the
|
|
// simplification note removing better-session).
|
|
assert.doesNotMatch(patch, /@morlay\//, 'the deprecated better-session integration must not reappear in the aggregate patch')
|
|
assert.doesNotMatch(patch, /^- id: web-ui-(session-branch|session-rdb|conversation-message-actions)$/m, 'better-session sub-plugin rows must not mount')
|
|
assert.doesNotMatch(patch, /@linxin666\/dsh-perf/, 'the removed dsh-perf plugin must not reappear in the aggregate patch')
|
|
})
|