Files
apes-sidebar/tests/market-manifest.spec.ts
T
Menghuan1918 bd11bad65c feat: adapt to DSH 0.1.7-rc.1
Move the support line from 0.1.7-alpha.1 to 0.1.7-rc.1 (peer floor
`^0.1.7-rc.1`). 0.21.0-alpha.1 never reached npm, and once the code-card
labels below are fixed the same build crashes on alpha.1 (its DiffBlock
still reads the `labels.files` upstream deleted), so declaring only the
host we verify against is the honest floor — and from rc.1 on the host
enforces peers at boot anyway: a row whose `@deepseek-ai/dsh-*` peers do
not satisfy the runtime is silently disabled.

Dependencies: 28 devDependency pins and 14 peers move to 0.1.7-rc.1.
rc.1 also raised the vendored runtime (cordis 4.0.4, schemastery 3.18.4,
cordis-plugin-group 1.0.4, cordis-plugin-include 1.0.9) and publishes DSH
cross-references as exact versions, so bumping schemastery/cordis alone
left four unmet transitive peers — the three vendor packages are hoisted
into devDependencies, which is what makes `pnpm peers check` clean again.

One compile break: rc.1 added `CodeToolbarLabels` (codeLabel / wrapLabel /
unwrapLabel, all required), made DiffBlockLabels / ReadBlockLabels extend
it and deleted `DiffBlockLabels.files`. Fixing SideChatView then leaving
markdown on the legacy banner would render two different code-card styles
inside one side-chat view, so the plugin adopts the card everywhere:
`markdownTextProps()` now passes `labels.code.toolbarLabels` with the
host's own wording (codeBlock.title|wrap|unwrap, added to all 20
dictionaries; `sideChatBlockFiles` retires with the deleted footer).

Adopting the card surfaced a silent regression the suite caught: the new
toolbar shows `codeLabel` for languages the highlighter does not know, and
mermaid has no shiki grammar, so the info string no longer reaches the DOM
and the banner-text probe in mermaid.tsx matched nothing — mermaid fences
stopped being swapped for diagrams. Detection now matches the FENCE BODY
from the source that produced the render.

`tests/market-manifest.spec.ts` pins every DSH peer as a floating caret
range: an exact pin (the devDependency style) or a comparator without a
prerelease would fail rc.1's preflight and silently stop the plugin from
loading, and `pnpm peers check` cannot see that — it validates the
resolved dev tree, not the range shape the host reads.
2026-09-23 22:24:56 +08:00

138 lines
6.8 KiB
TypeScript

/**
* DSH community-market compatibility guard: keeps the npm package inside the
* "Host 接受什么" rules of dsh-community-market's
* docs/install-and-uninstall.zh.md, so the catalog's verified npm install
* target (repository_backlink) keeps passing for this package:
*
* - no `cordis` entry in dependencies / peerDependencies /
* optionalDependencies (the market preview hard-rejects the legacy Cordis
* runtime name in any of the three fields, even when optional),
* - no install lifecycle scripts (preinstall / install / postinstall /
* prepare) in the **packed** manifest — the repo manifest legitimately
* keeps `prepare` for git-install builds; pnpm strips it from the published
* surface, and this guard verifies that stripped surface stays clean,
* - a safe, bund-visible `dsh.bundle.patch` path (`./cordis.patch.yml`),
* - a repository identity that normalizes to a credential-free HTTPS GitHub
* owner/repo URL (the catalog ↔ npm backlink comparison),
* - an exact SemVer version (no ranges, no tags; a prerelease suffix is
* allowed — prerelease releases ride the npm `alpha` dist-tag and are not
* the market's verified `latest` install target until a stable cut).
*
* The packed-manifest checks run `pnpm pack` into a temp dir (the `prepare`
* script rebuilds the gitignored lib/, like the release flow does).
*/
import { execFileSync } from 'node:child_process'
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
const ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)))
const pkg = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf8')) as {
name: string
version: string
repository?: { type?: string; url?: string }
engines?: { node?: string }
dsh?: { bundle?: { patch?: string } }
scripts?: Record<string, string>
dependencies?: Record<string, string>
peerDependencies?: Record<string, string>
optionalDependencies?: Record<string, string>
}
/** The market's lifecycle-script reject list (install-and-uninstall.zh.md). */
const LIFECYCLE_SCRIPTS = ['preinstall', 'install', 'postinstall', 'prepare'] as const
/** Exact SemVer: exactly three numeric segments, optional prerelease suffix
* (no range / tag — prerelease cuts publish under the npm `alpha` dist-tag). */
const EXACT_SEMVER = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u
/** Read `package.json` from a fresh `pnpm pack` tarball (the publish surface). */
function packedManifest(): Record<string, unknown> {
const dir = mkdtempSync(join(tmpdir(), 'market-pack-'))
try {
// On Windows `pnpm` ships as a `.cmd` shim that `execFileSync` cannot
// spawn directly (ENOENT). Run it through the shell so the same call
// works on every platform; `tar` is available on all supported OSes.
execFileSync('pnpm', ['pack', '--pack-destination', dir], { cwd: ROOT, stdio: 'pipe', shell: process.platform === 'win32' })
// Never hand tar an absolute Windows path: Git Bash's GNU tar reads the
// drive-letter colon as a remote-host spec ("Cannot connect to C:").
// Run it from the pack dir with the bare tarball name instead.
execFileSync('tar', ['-xzf', `${pkg.name}-${pkg.version}.tgz`, 'package/package.json'], { cwd: dir, stdio: 'pipe' })
return JSON.parse(readFileSync(join(dir, 'package/package.json'), 'utf8')) as Record<string, unknown>
} finally {
rmSync(dir, { recursive: true, force: true })
}
}
describe('DSH community-market manifest compatibility', () => {
it('declares no cordis entry in any dependency field (dependencies / peerDependencies / optionalDependencies)', () => {
for (const field of ['dependencies', 'peerDependencies', 'optionalDependencies'] as const) {
const deps = pkg[field]
if (deps === undefined) continue
expect(Object.keys(deps), field).not.toContain('cordis')
}
})
it('declares no install lifecycle scripts in the PACKED manifest (the published surface)', () => {
const packed = packedManifest()
const scripts = (packed.scripts as Record<string, string> | undefined) ?? {}
for (const script of LIFECYCLE_SCRIPTS) {
expect(scripts, script).not.toHaveProperty(script)
}
}, 180000)
it('declares a safe bundle patch that exists inside the package', () => {
const patch = pkg.dsh?.bundle?.patch
expect(patch, 'dsh.bundle.patch').toBeDefined()
const path = patch!
expect(path.startsWith('./'), 'must be a relative path').toBe(true)
const relative = path.slice(2)
expect(relative).not.toBe('')
expect(relative.startsWith('/')).toBe(false)
expect(relative.includes('\\')).toBe(false)
expect(relative.split('/').every(segment => segment.length > 0 && segment !== '.' && segment !== '..'))
.toBe(true)
expect(existsSync(resolve(ROOT, relative)), relative).toBe(true)
})
it('declares a repository identity the market can normalize to a credential-free HTTPS GitHub owner/repo URL', () => {
const repository = pkg.repository
expect(repository, 'repository').toBeDefined()
const url = new URL(repository!.url!)
expect(url.protocol).toBe('https:')
expect(url.username).toBe('')
expect(url.password).toBe('')
expect(url.search).toBe('')
expect(url.hash).toBe('')
expect(url.hostname.toLowerCase()).toBe('github.com')
expect(url.pathname.split('/').filter(Boolean)).toHaveLength(2)
})
it('declares an exact SemVer version (no range or tag; prerelease allowed for the alpha track)', () => {
expect(pkg.version).toMatch(EXACT_SEMVER)
})
it('keeps every DSH peer a floating range the host runtime can satisfy', () => {
// DSH 0.1.7-rc.1 added a startup compatibility preflight
// (packages/boot/app-boot/src/plugin-compatibility.ts): for every peer
// named `@deepseek-ai/dsh` or `@deepseek-ai/dsh-*`, the host evaluates
// `semver.satisfies(runtimeVersion, range, { includePrerelease: true })`
// and DISABLES the whole profile row when it fails — one stderr line in a
// log nobody reads, no dialog, and `peerDependenciesMeta.optional` grants
// no exemption. An exact pin (the style devDependencies use) or a
// comparator carrying no prerelease (`^0.1.7`) would therefore make the
// plugin silently stop loading, so the published ranges must stay carets
// on the baseline tuple. `pnpm peers check` cannot see this: it validates
// the resolved dev tree, not the range shape a host will read.
const peers = pkg.peerDependencies ?? {}
const dshPeers = Object.keys(peers).filter(name => name === '@deepseek-ai/dsh' || name.startsWith('@deepseek-ai/dsh-'))
expect(dshPeers.length).toBeGreaterThan(10)
for (const name of dshPeers) {
expect(peers[name], name).toMatch(/^\^0\.1\.7-[a-z0-9.]+$/)
}
})
})