Merge remote-tracking branch 'origin/master' into worktree/pr468-retarget-latest-master

# Conflicts:
#	docs/config-catalog.md
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	packages/examples/tui-demo/src/index.ts
#	packages/examples/tui-demo/tests/tui-agent.spec.ts
#	packages/ui/tui/README.md
#	packages/ui/tui/package.json
#	packages/ui/tui/src/index.ts
#	packages/ui/tui/tests/tui.spec.ts
#	packages/ui/tui/tsconfig.json
#	pnpm-lock.yaml
#	vitest.config.ts
This commit is contained in:
Tianyi Cui
2026-07-22 21:27:18 +08:00
230 changed files with 8640 additions and 2500 deletions

284
scripts/install.sh Executable file
View File

@@ -0,0 +1,284 @@
#!/bin/sh
# dsh one-line installer.
#
# curl -fsSL https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/master/scripts/install.sh | sh
#
# It clones the harness to ~/.dsh/source, checks host dependencies (git, Node,
# pnpm) and offers to install a missing pnpm, runs `pnpm install` (no build —
# the `bin/dsh` launcher runs the TypeScript source through the repo's own tsx),
# symlinks `dsh` onto PATH, records your API credentials in the Harness home
# (`~/.dsh`) dsh reads at boot, and drops you into `dsh`.
#
# When run from inside an existing checkout (e.g. `sh scripts/install.sh` rather
# than `curl ... | sh`) it reuses that checkout and skips the clone/update, leaving
# the working tree untouched; DSH_REF is ignored in that mode. Setting DSH_SOURCE
# to a different directory opts back into the normal clone/update path.
#
# When run through `curl | sh` the script text arrives on stdin, so every
# prompt and the final launch read the controlling terminal (/dev/tty) directly;
# with no terminal the script prints the manual next steps instead.
#
# Overridable via environment:
# DSH_REF branch or tag to clone/checkout (default: master)
# DSH_REPO clone URL (default: the GitHub repo)
# DSH_SOURCE checkout location (default: ~/.dsh/source)
# DSH_BIN_DIR directory the `dsh` symlink lands in (default: ~/.local/bin)
# DSH_HOME Harness home holding the personal config (default: ~/.dsh)
# FIXME(install-ts): Move the post-checkout workflow into a tested TypeScript
# entrypoint; keep this POSIX shell file as the curl/source bootstrap.
set -eu
DSH_REF=${DSH_REF:-master}
DSH_REPO=${DSH_REPO:-https://github.com/deepseek-harness/deepseek-harness.git}
# Remember whether the caller pinned a source location before defaulting it, so
# in-repo detection only repoints an unset DSH_SOURCE.
if [ -n "${DSH_SOURCE:-}" ]; then DSH_SOURCE_EXPLICIT=1; else DSH_SOURCE_EXPLICIT=0; fi
DSH_SOURCE=${DSH_SOURCE:-$HOME/.dsh/source}
DSH_BIN_DIR=${DSH_BIN_DIR:-$HOME/.local/bin}
# --- in-repo detection ---------------------------------------------------------
# Under `curl ... | sh` the script text arrives on stdin, so $0 is the shell
# name and no file path resolves; running a checked-out copy (`sh
# scripts/install.sh`) makes $0 the script file. When $0 is a readable file whose
# parent is a scripts/ dir inside a real dsh checkout (bin/dsh launcher present),
# reuse that checkout and skip the clone. An explicit DSH_SOURCE pointing
# elsewhere opts back into the clone/update path.
IN_REPO=0
if [ -f "$0" ]; then
_self_dir=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd -P) || _self_dir=''
if [ -n "$_self_dir" ]; then
_repo_root=$(dirname -- "$_self_dir")
if [ "$(basename -- "$_self_dir")" = scripts ] \
&& [ -x "$_repo_root/bin/dsh" ] && [ -f "$_repo_root/scripts/install.sh" ]; then
if [ "$DSH_SOURCE_EXPLICIT" = 0 ] || [ "$DSH_SOURCE" = "$_repo_root" ]; then
IN_REPO=1
DSH_SOURCE=$_repo_root
fi
fi
fi
fi
# --- terminal-aware prompting --------------------------------------------------
# stdin is the piped script, so read the controlling terminal for input.
if { true </dev/tty; } 2>/dev/null; then
HAS_TTY=1
# Restore terminal echo on exit or interrupt: ask_secret disables echo between
# its stty toggles, and dash (a common `sh`) does not run an EXIT trap when the
# shell is killed by a signal, so the fatal signals need their own handler. A
# successful run ends in exec, which replaces this process and drops the traps.
trap 'stty echo </dev/tty 2>/dev/null || true' EXIT
trap 'stty echo </dev/tty 2>/dev/null || true; exit 130' INT TERM HUP
else
HAS_TTY=0
fi
# Colour only when writing to a terminal.
if [ -t 1 ]; then
B=$(printf '\033[1m'); DIM=$(printf '\033[2m'); RED=$(printf '\033[31m')
GRN=$(printf '\033[32m'); YEL=$(printf '\033[33m'); RST=$(printf '\033[0m')
else
B=''; DIM=''; RED=''; GRN=''; YEL=''; RST=''
fi
info() { printf '%s==>%s %s\n' "$GRN" "$RST" "$1"; }
step() { printf '\n%s==>%s %s%s%s\n' "$GRN" "$RST" "$B" "$1" "$RST"; }
warn() { printf '%s warn%s %s\n' "$YEL" "$RST" "$1" >&2; }
die() { printf '%serror%s %s\n' "$RED" "$RST" "$1" >&2; exit 1; }
# ask PROMPT [DEFAULT] -> answer on stdout (plain-text line).
ask() {
[ "$HAS_TTY" = 1 ] || die "no terminal available for input; re-run in an interactive shell"
printf '%s%s%s ' "$B" "$1" "$RST" >/dev/tty
IFS= read -r _ans </dev/tty || _ans=''
[ -n "$_ans" ] || _ans=${2:-}
printf '%s' "$_ans"
}
# ask_secret PROMPT -> answer on stdout, with terminal echo suppressed.
ask_secret() {
[ "$HAS_TTY" = 1 ] || die "no terminal available for input; re-run in an interactive shell"
printf '%s%s%s ' "$B" "$1" "$RST" >/dev/tty
stty -echo </dev/tty 2>/dev/null || true
IFS= read -r _sec </dev/tty || _sec=''
stty echo </dev/tty 2>/dev/null || true
printf '\n' >/dev/tty
printf '%s' "$_sec"
}
# confirm PROMPT [Y] -> exit 0 on yes. Default is no unless second arg is "Y".
confirm() {
_def=${2:-N}
if [ "$HAS_TTY" != 1 ]; then
[ "$_def" = Y ] # non-interactive: take the default
return
fi
if [ "$_def" = Y ]; then _hint='[Y/n]'; else _hint='[y/N]'; fi
printf '%s%s%s %s ' "$B" "$1" "$RST" "$_hint" >/dev/tty
IFS= read -r _r </dev/tty || _r=''
[ -n "$_r" ] || _r=$_def
case "$_r" in [yY]|[yY][eE][sS]) return 0 ;; *) return 1 ;; esac
}
printf '%s\n' "${B}DeepSeek Harness — dsh installer${RST}"
printf '%ssource %s @ %s%s\n' "$DIM" "$DSH_SOURCE" "$DSH_REF" "$RST"
# --- 1. dependency check -------------------------------------------------------
step "Checking dependencies"
command -v git >/dev/null 2>&1 || die "git is required but not found. Install git, then re-run."
info "git ... ok"
# Node ^22.19.0 || >=24.0.0 (see the root package.json "engines" field).
node_ok() {
command -v node >/dev/null 2>&1 || return 1
_v=$(node -v 2>/dev/null) || return 1
_v=${_v#v}
_major=${_v%%.*}
_rest=${_v#*.}
_minor=${_rest%%.*}
case "$_major" in ''|*[!0-9]*) return 1 ;; esac
case "$_minor" in ''|*[!0-9]*) _minor=0 ;; esac
[ "$_major" -ge 24 ] && return 0
[ "$_major" -eq 22 ] && [ "$_minor" -ge 19 ] && return 0
return 1
}
if node_ok; then
info "node $(node -v) ... ok"
else
if command -v node >/dev/null 2>&1; then
die "Node $(node -v) is unsupported. dsh needs ^22.19.0 || >=24.0.0 — upgrade Node, then re-run."
fi
die "Node is required but not found. Install Node ^22.19.0 || >=24, then re-run."
fi
# pnpm is the only dependency we offer to install for you.
if command -v pnpm >/dev/null 2>&1; then
info "pnpm $(pnpm --version 2>/dev/null) ... ok"
else
warn "pnpm is not installed."
if confirm "Install pnpm now?" Y; then
if command -v corepack >/dev/null 2>&1 && corepack enable pnpm >/dev/null 2>&1; then
info "enabled pnpm via corepack"
elif command -v npm >/dev/null 2>&1 && npm install -g pnpm >/dev/null 2>&1; then
info "installed pnpm via npm"
else
die "could not install pnpm automatically. Install it (https://pnpm.io/installation), then re-run."
fi
command -v pnpm >/dev/null 2>&1 || die "pnpm still not on PATH after install. Open a new shell, then re-run."
else
die "pnpm is required. Install it (https://pnpm.io/installation), then re-run."
fi
fi
# --- 2. clone (or update) the source ------------------------------------------
if [ "$IN_REPO" = 1 ]; then
step "Using existing checkout at $DSH_SOURCE"
info "running from inside the repo — skipping clone (DSH_REF ignored, working tree left untouched)"
else
step "Fetching source into $DSH_SOURCE"
if [ -d "$DSH_SOURCE/.git" ]; then
info "existing checkout found — updating"
git -C "$DSH_SOURCE" fetch --depth 1 origin "$DSH_REF"
# Reset the checkout to the freshly fetched tip. FETCH_HEAD (not
# origin/<ref>) so this resolves for a tag as well as a branch, and -B makes
# the re-run idempotent whether or not DSH_REF changed since the last install.
git -C "$DSH_SOURCE" checkout -q -B "$DSH_REF" FETCH_HEAD
else
mkdir -p "$(dirname "$DSH_SOURCE")"
git clone --depth 1 --branch "$DSH_REF" "$DSH_REPO" "$DSH_SOURCE"
fi
fi
# --- 3. install dependencies (no build; the launcher runs from source) --------
step "Installing dependencies with pnpm (this can take a while)"
( cd "$DSH_SOURCE" && pnpm install )
[ -x "$DSH_SOURCE/bin/dsh" ] || die "launcher $DSH_SOURCE/bin/dsh missing after install — is DSH_REF a branch that ships apps/cli?"
# --- 4. put `dsh` on PATH ------------------------------------------------------
step "Linking dsh into $DSH_BIN_DIR"
mkdir -p "$DSH_BIN_DIR"
ln -sf "$DSH_SOURCE/bin/dsh" "$DSH_BIN_DIR/dsh"
info "linked $DSH_BIN_DIR/dsh -> $DSH_SOURCE/bin/dsh"
case ":$PATH:" in
*":$DSH_BIN_DIR:"*) ON_PATH=1 ;;
*) ON_PATH=0 ;;
esac
if [ "$ON_PATH" = 0 ]; then
warn "$DSH_BIN_DIR is not on your PATH."
_line="export PATH=\"$DSH_BIN_DIR:\$PATH\""
_rc=''
_sh=${SHELL:-} # SHELL may be unset; word-removal on an unset var trips set -u under dash.
case "${_sh##*/}" in
zsh) _rc="$HOME/.zshrc" ;;
bash) _rc="$HOME/.bashrc" ;;
esac
if [ -n "$_rc" ] && [ -f "$_rc" ] && grep -qF "$_line" "$_rc" 2>/dev/null; then
info "$_rc already exports $DSH_BIN_DIR — open a new shell to pick it up"
elif [ -n "$_rc" ] && confirm "Add it to $_rc?" Y; then
printf '\n# Added by the dsh installer\n%s\n' "$_line" >>"$_rc"
info "updated $_rc — run 'source $_rc' or open a new shell to pick it up"
else
warn "add this line to your shell profile yourself:"
printf ' %s\n' "$_line"
fi
fi
# --- 5. credentials ------------------------------------------------------------
# Mirror app-boot's resolveDshHome precedence ($DSH_HOME, else ~/.dsh) so creds land where dsh reads them.
if [ -n "${DSH_HOME:-}" ]; then
CONF="$DSH_HOME"
else
CONF="$HOME/.dsh"
fi
ENV_FILE="$CONF/.env"
step "Configuring credentials"
if [ -f "$ENV_FILE" ] && grep -q '^DEEPSEEK_API_KEY=' "$ENV_FILE" 2>/dev/null; then
info "DEEPSEEK_API_KEY already set in $ENV_FILE"
if ! confirm "Replace it?" N; then
SKIP_CREDS=1
fi
fi
if [ "${SKIP_CREDS:-0}" != 1 ]; then
if [ "$HAS_TTY" = 1 ]; then
API_KEY=$(ask_secret "DeepSeek API key (input hidden):")
if [ -z "$API_KEY" ]; then
warn "no key entered — skipping. Set DEEPSEEK_API_KEY in $ENV_FILE before using dsh."
else
BASE_URL=$(ask "DeepSeek base URL (optional, Enter to skip):")
mkdir -p "$CONF"
# The installer owns exactly the two DEEPSEEK_* lines; any other lines the
# user keeps in this .env are preserved. The rewrite happens in a subshell
# so umask 077 (which closes the create-time permission race) does not leak
# into the exec'd dsh, and lands atomically via a same-dir temp + mv.
_tmp="$ENV_FILE.dsh.$$"
(
umask 077
if [ -f "$ENV_FILE" ]; then
grep -v -e '^DEEPSEEK_API_KEY=' -e '^DEEPSEEK_BASE_URL=' "$ENV_FILE" >"$_tmp" || true
else
: >"$_tmp"
fi
printf 'DEEPSEEK_API_KEY=%s\n' "$API_KEY" >>"$_tmp"
if [ -n "$BASE_URL" ]; then printf 'DEEPSEEK_BASE_URL=%s\n' "$BASE_URL" >>"$_tmp"; fi
)
mv "$_tmp" "$ENV_FILE"
chmod 600 "$ENV_FILE" 2>/dev/null || true
info "wrote $ENV_FILE"
fi
else
warn "no terminal for credential input — set DEEPSEEK_API_KEY in $ENV_FILE before using dsh."
fi
fi
# --- 6. launch -----------------------------------------------------------------
step "Done"
if [ "$HAS_TTY" = 1 ]; then
info "launching dsh — run 'dsh' anytime to start again"
exec "$DSH_BIN_DIR/dsh" </dev/tty
else
info "install complete. Start it with:"
printf ' %s\n' "$DSH_BIN_DIR/dsh"
fi

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
# Ubuntu's package transaction scans the hosted image's full dpkg database and
# runs post-install hooks. CI needs only the signed-archive payload, so pin and
# verify that payload before extracting it into the ephemeral runner directory.
readonly BUBBLEWRAP_VERSION='0.9.0-1ubuntu0.1'
readonly BUBBLEWRAP_SHA256='1b506492bd9c7fd0cdb4f02ac822f1d3e336b0aead5113c1239baf8db5db562a'
readonly BUBBLEWRAP_URL="https://archive.ubuntu.com/ubuntu/pool/main/b/bubblewrap/bubblewrap_${BUBBLEWRAP_VERSION}_amd64.deb"
: "${RUNNER_TEMP:?prepare-ci-bubblewrap requires RUNNER_TEMP}"
: "${GITHUB_PATH:?prepare-ci-bubblewrap requires GITHUB_PATH}"
if [[ "$(uname -s)" != 'Linux' || "$(uname -m)" != 'x86_64' ]]; then
echo 'prepare-ci-bubblewrap supports only Linux x86_64 hosted runners' >&2
exit 1
fi
archive="${RUNNER_TEMP}/bubblewrap_${BUBBLEWRAP_VERSION}_amd64.deb"
root="${RUNNER_TEMP}/dsh-bubblewrap"
curl --fail --silent --show-error --location --retry 3 --output "$archive" "$BUBBLEWRAP_URL"
printf '%s %s\n' "$BUBBLEWRAP_SHA256" "$archive" | sha256sum --check --status
mkdir -p "$root"
dpkg-deb --extract "$archive" "$root"
printf '%s\n' "$root/usr/bin" >> "$GITHUB_PATH"
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 \
|| echo 'apparmor userns knob absent — the functional probe decides'
"$root/usr/bin/bwrap" --version
"$root/usr/bin/bwrap" --ro-bind / / --dev /dev --proc /proc --die-with-parent -- true
echo 'bubblewrap functional probe passed'

View File

@@ -1,13 +1,19 @@
/** Tests for the documentation website projection adapter. */
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { join, resolve } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { docsPages, type DocsPage } from '../website/docs.ts'
import { addProjectionFrontmatter, projectedPageContent, rewriteMarkdown } from './project-doc-site.ts'
const roots: string[] = []
const repositoryRoot = resolve(import.meta.dirname, '..')
function unexpectedWebsiteMarkdown(files: readonly string[]): string[] {
return files.filter(file => file.endsWith('.md') && file !== 'website/AGENTS.md').sort()
}
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
@@ -34,6 +40,29 @@ function fixture(): { root: string; pages: DocsPage[] } {
}
}
describe('website source layout', () => {
it('rejects Markdown outside the subtree instructions', () => {
expect(unexpectedWebsiteMarkdown([
'website/AGENTS.md',
'website/docs.ts',
'website/zh-CN/api/harness/service.md',
])).toEqual(['website/zh-CN/api/harness/service.md'])
})
it('contains no tracked or unignored documentation copies', () => {
const files = execFileSync(
'git',
['ls-files', '--cached', '--others', '--exclude-standard', '--', 'website'],
{ cwd: repositoryRoot, encoding: 'utf8' },
).split('\n').filter(file => file !== '' && existsSync(resolve(repositoryRoot, file)))
expect(
unexpectedWebsiteMarkdown(files),
'Keep canonical Markdown under docs/ and publish it through website/docs.ts.',
).toEqual([])
})
})
describe('rewriteMarkdown', () => {
it('maps published pages and pins unpublished source links', () => {
const { root, pages } = fixture()

View File

@@ -0,0 +1,61 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { spawnSync } from 'node:child_process'
import { afterEach, describe, expect, it } from 'vitest'
const repositoryRoot = fileURLToPath(new URL('..', import.meta.url))
const runner = fileURLToPath(new URL('./publint-all.ts', import.meta.url))
const roots: string[] = []
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
})
function fixture(exportPath = './lib/index.js'): string {
const root = mkdtempSync(join(tmpdir(), 'dsh-publint-all-'))
roots.push(root)
const packageDir = join(root, 'packages/core/probe')
mkdirSync(join(packageDir, 'lib'), { recursive: true })
writeFileSync(join(packageDir, 'package.json'), `${JSON.stringify({
name: '@deepseek-ai/dsh-probe',
version: '0.0.1',
type: 'module',
license: 'MIT',
engines: { node: '>=22.19' },
sideEffects: false,
files: ['lib'],
exports: { '.': { default: exportPath } },
}, null, 2)}\n`)
writeFileSync(join(packageDir, 'README.md'), '# Probe\n')
writeFileSync(join(packageDir, 'lib/index.js'), 'export const probe = true\n')
writeFileSync(join(packageDir, 'unpublished.js'), 'export const hidden = true\n')
return root
}
function run(root: string) {
return spawnSync(process.execPath, [
'--import', 'tsx', runner,
'--packages-root', root,
], {
cwd: repositoryRoot,
encoding: 'utf8',
timeout: 5_000,
})
}
describe('publint package runner', () => {
it('lints recursively declared files from an in-memory publication view', () => {
const result = run(fixture())
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain('linting 1 package(s)')
expect(result.stdout).toContain('All good!')
})
it('rejects an export that exists in the workspace but is not published', () => {
const result = run(fixture('./unpublished.js'))
expect(result.status).toBe(1)
expect(result.stdout).toContain('unpublished.js')
})
})

View File

@@ -1,46 +1,53 @@
import { execFile } from 'node:child_process'
import { existsSync, readdirSync } from 'node:fs'
/** Run publint over the exact manifest-declared publication view of every package. */
import {
globSync,
readFileSync,
readdirSync,
statSync,
} from 'node:fs'
import { availableParallelism } from 'node:os'
import { resolve } from 'node:path'
import { promisify } from 'node:util'
import { dirname, relative, resolve, sep } from 'node:path'
import { publint, type Message, type PackFile } from 'publint'
import { formatMessage } from 'publint/utils'
const execFileAsync = promisify(execFile)
const CONCURRENCY_ENV = 'DSH_PUBLINT_CONCURRENCY'
const repositoryRoot = resolve(import.meta.dirname, '..')
const options = parseOptions(process.argv.slice(2))
const packagesRoot = resolve(options.get('--packages-root') ?? repositoryRoot)
// Discover harness packages at packages/<group>/<pkg>; group containers,
// examples, and private vendored sources are not package targets.
const root = resolve(import.meta.dirname, '..')
const packagesRoot = resolve(root, 'packages')
interface PackageTarget {
path: string
directory: string
manifest: PackageManifest
}
// Run publint's JS CLI through the current node, not the .bin shim: the
// extensionless shim isn't spawnable on Windows (CVE-2024-27980) and the .cmd
// variant needs shell:true, which space-joins args UNESCAPED (DEP0190) and
// breaks when the repo path contains spaces. The JS entry is identical on every
// platform (`bin` is `./src/cli.js` per publint's package.json).
const publintCli = resolve(root, 'node_modules/publint/src/cli.js')
interface PackageManifest {
name?: string
files?: unknown
}
type PublintResult =
| { path: string; status: 'passed'; stdout: string; stderr: string }
| { path: string; status: 'failed'; stdout: string; stderr: string; message: string }
| { path: string; status: 'passed'; messages: Message[]; manifest: Record<string, unknown> }
| { path: string; status: 'failed'; messages: Message[]; manifest: Record<string, unknown>; failure?: string }
function workspacePackages(): string[] {
return readdirSync(packagesRoot, { withFileTypes: true })
.filter(group => group.isDirectory())
.flatMap(group =>
readdirSync(resolve(packagesRoot, group.name), { withFileTypes: true })
.filter(pkg => pkg.isDirectory())
.filter(pkg => existsSync(resolve(packagesRoot, group.name, pkg.name, 'package.json')))
.map(pkg => `packages/${group.name}/${pkg.name}`),
)
function workspacePackages(): PackageTarget[] {
return globSync('packages/*/*/package.json', { cwd: packagesRoot })
.sort()
.map((manifestPath) => {
const absoluteManifestPath = resolve(packagesRoot, manifestPath)
const manifest = JSON.parse(readFileSync(absoluteManifestPath, 'utf8')) as PackageManifest
return { path: dirname(manifestPath), directory: dirname(absoluteManifestPath), manifest }
})
}
function publintConcurrency(total: number): number {
if (total === 0) return 0
const raw = process.env[CONCURRENCY_ENV]
if (raw !== undefined) {
if (raw !== undefined && raw !== '') {
const parsed = Number.parseInt(raw, 10)
if (!Number.isSafeInteger(parsed) || parsed < 1) {
if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) {
throw new Error(`publint-all: ${CONCURRENCY_ENV} must be a positive integer, got ${JSON.stringify(raw)}.`)
}
return Math.min(total, parsed)
@@ -49,57 +56,106 @@ function publintConcurrency(total: number): number {
return Math.min(total, availableParallelism())
}
function outputText(value: unknown): string {
if (typeof value === 'string') return value
if (Buffer.isBuffer(value)) return value.toString()
return ''
function publicationFiles(target: PackageTarget): PackFile[] {
const paths = new Set<string>()
addPath(resolve(target.directory, 'package.json'), paths)
const declared = Array.isArray(target.manifest.files)
? target.manifest.files.filter((value): value is string => typeof value === 'string')
: []
for (const pattern of [
...declared,
'README*',
'LICENSE*',
'LICENCE*',
'CHANGELOG*',
'CHANGES*',
'HISTORY*',
'NOTICE*',
]) {
for (const match of globSync(pattern, { cwd: target.directory })) {
addPath(resolve(target.directory, match), paths)
}
}
return [...paths]
.sort()
.map(path => ({
name: `package/${relative(target.directory, path).split(sep).join('/')}`,
data: readFileSync(path),
}))
}
async function runPublint(path: string): Promise<PublintResult> {
function addPath(path: string, paths: Set<string>): void {
const stat = statSync(path)
if (stat.isDirectory()) {
for (const entry of readdirSync(path)) addPath(resolve(path, entry), paths)
} else if (stat.isFile()) {
paths.add(path)
}
}
async function runPublint(target: PackageTarget): Promise<PublintResult> {
try {
const { stdout, stderr } = await execFileAsync(process.execPath, [publintCli, path], {
cwd: root,
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024,
const result = await publint({
pkgDir: 'package',
pack: { files: publicationFiles(target) },
})
return { path, status: 'passed', stdout, stderr }
const manifest = result.pkg as Record<string, unknown>
return result.messages.some(message => message.type === 'error')
? { path: target.path, status: 'failed', messages: result.messages, manifest }
: { path: target.path, status: 'passed', messages: result.messages, manifest }
} catch (error: unknown) {
const failed = error as { stdout?: unknown; stderr?: unknown; message?: string }
return {
path,
path: target.path,
status: 'failed',
stdout: outputText(failed.stdout),
stderr: outputText(failed.stderr),
message: failed.message ?? 'publint failed',
messages: [],
manifest: target.manifest as Record<string, unknown>,
failure: error instanceof Error ? error.message : String(error),
}
}
}
async function runAll(paths: string[], concurrency: number): Promise<PublintResult[]> {
async function runAll(targets: PackageTarget[], concurrency: number): Promise<PublintResult[]> {
let next = 0
const results: Array<PublintResult | undefined> = []
await Promise.all(Array.from({ length: concurrency }, async () => {
for (;;) {
const index = next
next += 1
const path = paths[index]
if (path === undefined) return
results[index] = await runPublint(path)
const target = targets[index]
if (target === undefined) return
results[index] = await runPublint(target)
}
}))
return paths.map((path, index) => {
return targets.map((target, index) => {
const result = results[index]
if (result === undefined) throw new Error(`publint-all: missing result for ${path}.`)
if (result === undefined) throw new Error(`publint-all: missing result for ${target.path}.`)
return result
})
}
function printResult(result: PublintResult): void {
console.log(`Running publint for ${result.path}...`)
process.stdout.write(result.stdout)
process.stderr.write(result.stderr)
if (result.status === 'failed') console.error(result.message)
if ('failure' in result) console.error(result.failure)
for (const message of result.messages) {
console.log(formatMessage(message, result.manifest, { color: false }) ?? message.code)
}
if (result.status === 'passed' && result.messages.length === 0) console.log('All good!')
}
function parseOptions(args: string[]): Map<string, string> {
const parsed = new Map<string, string>()
for (let index = 0; index < args.length; index += 2) {
const name = args[index]
const value = args[index + 1]
if (name !== '--packages-root' || value === undefined || value.startsWith('--')) {
throw new Error(`publint-all: expected [--packages-root PATH], got ${JSON.stringify(args)}.`)
}
if (parsed.has(name)) throw new Error(`publint-all: duplicate option ${name}.`)
parsed.set(name, value)
}
return parsed
}
const packages = workspacePackages()

View File

@@ -16,6 +16,9 @@ type Mode =
| 'ci-coverage'
| 'ci-snapshot'
| 'ci-artifacts'
| 'ci-windows-blocking'
| 'ci-windows-complete'
| 'ci-windows-observational'
| 'node-compat'
| 'pre-push'
| 'manual-push'
@@ -32,6 +35,7 @@ interface Gate {
env?: Record<string, string | undefined>
input?: string
verify?: (result: GateResult) => Promise<void>
allowFailure?: boolean
}
interface GateResult {
@@ -77,7 +81,9 @@ console.log(`run-gates: ${mode} running ${gates.length} gate(s) with ${maxConcur
const results = await runGates(gates, maxConcurrency)
printSummary(results, performance.now() - startedAt)
if (results.some(result => result.status === 'failed' || result.status === 'skipped')) process.exit(1)
if (results.some(result => result.gate.allowFailure !== true && (result.status === 'failed' || result.status === 'skipped'))) {
process.exit(1)
}
function parseMode(raw: string | undefined): Mode {
switch (raw) {
@@ -87,13 +93,17 @@ function parseMode(raw: string | undefined): Mode {
case 'ci-coverage':
case 'ci-snapshot':
case 'ci-artifacts':
case 'ci-windows-blocking':
case 'ci-windows-complete':
case 'ci-windows-observational':
case 'node-compat':
case 'pre-push':
case 'manual-push':
case 'doc-sync':
return raw
default:
throw new Error(
`run-gates: expected mode ci-primary | ci-static | ci-lint | ci-coverage | ci-snapshot | ci-artifacts | node-compat | pre-push | doc-sync, got ${JSON.stringify(raw)}.`,
`run-gates: expected mode ci-primary | ci-static | ci-lint | ci-coverage | ci-snapshot | ci-artifacts | ci-windows-blocking | ci-windows-complete | ci-windows-observational | node-compat | pre-push | manual-push | doc-sync, got ${JSON.stringify(raw)}.`,
)
}
}
@@ -167,31 +177,19 @@ function gatesForMode(selected: Mode): Gate[] {
pnpmScript('duplication', 'duplication'),
]
case 'ci-coverage':
return [
pnpmScript('build', 'build'),
coverageGate(),
]
return [coverageGate()]
case 'ci-snapshot':
return [
pnpmScript('build', 'build'),
snapshotGate(),
]
return [pnpmScript('build', 'build'), snapshotGate()]
case 'ci-artifacts':
return ciArtifactGates()
case 'ci-windows-blocking':
return ciWindowsBlockingGates()
case 'ci-windows-complete':
return ciWindowsCompleteGates()
case 'ci-windows-observational':
return ciWindowsObservationalGates()
case 'node-compat':
return [
pnpmScript('typecheck', 'typecheck'),
pnpmExec('source-worker-smoke', [
'vitest',
'run',
'packages/workflow/workflow-workerthread/tests/source-worker.compat.spec.ts',
], { label: 'source worker smoke' }),
pnpmExec('jsonl-zstd-smoke', [
'vitest',
'run',
'packages/session-persistence/session-persistence-jsonl/tests/zstd.compat.spec.ts',
], { label: 'JSONL Zstandard smoke' }),
]
return nodeCompatGates()
case 'pre-push': return []
case 'manual-push':
return [
@@ -225,11 +223,12 @@ function ciPrimaryGates(): Gate[] {
lintGate(),
pnpmScript('duplication', 'duplication'),
coverageGate(),
...nodeCompatSmokeGates(),
snapshotGate(),
...docSyncLeafGates(),
pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }),
pnpmScript('knip', 'knip'),
pnpmScript('build', 'build', { needs: ['typecheck'] }),
pnpmScript('build', 'build'),
pnpmScript('publint', 'publint', { needs: ['build'] }),
pnpmScript('node-next-types', 'verify-node-next-types', {
label: 'node-next types',
@@ -240,13 +239,40 @@ function ciPrimaryGates(): Gate[] {
]
}
function nodeCompatGates(): Gate[] {
return [
...flagEnabled('DSH_NODE_COMPAT_SKIP_TYPECHECK') ? [] : [pnpmScript('typecheck', 'typecheck')],
...nodeCompatSmokeGates(),
]
}
function nodeCompatSmokeGates(): Gate[] {
return [
pnpmExec('source-worker-smoke', [
'vitest',
'run',
'packages/workflow/workflow-workerthread/tests/source-worker.compat.spec.ts',
], { label: 'source worker smoke' }),
pnpmExec('jsonl-zstd-smoke', [
'vitest',
'run',
'packages/session-persistence/session-persistence-jsonl/tests/zstd.compat.spec.ts',
], { label: 'JSONL Zstandard smoke' }),
]
}
function ciStaticGates(): Gate[] {
return [
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
pnpmScript('constraints', 'constraints'),
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }),
...docSyncLeafGates(),
pnpmScript('build', 'build'),
...docSyncLeafGates({
docTypecheckNeeds: ['build'],
docTypecheckEnv: { DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1' },
docsBuildScript: 'docs:build:mpa',
}),
pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }),
pnpmScript('knip', 'knip'),
]
@@ -265,11 +291,54 @@ function ciArtifactGates(): Gate[] {
]
}
function lintGate(): Gate {
function ciWindowsBlockingGates(): Gate[] {
return [
pnpmScript('windows-build', 'build', { label: 'build' }),
pnpmScript('windows-site', 'docs:build', { label: 'production site' }),
]
}
function ciWindowsCompleteGates(): Gate[] {
const observational = ciWindowsObservationalGates()
// The required production site replaces the observational MPA build; both
// VitePress modes write the same output directory and cannot overlap.
.filter(gate => gate.id !== 'build' && gate.id !== 'docs-site-build')
.map(gate => ({ ...gate, allowFailure: true }))
return [
pnpmScript('build', 'build'),
pnpmScript('windows-site', 'docs:build', { label: 'production site' }),
...observational,
]
}
function ciWindowsObservationalGates(): Gate[] {
return [
...ciStaticGates(),
lintGate(),
pnpmScript('duplication', 'duplication'),
{
...coverageGate(),
env: { DSH_EXAMPLE_MODE: 'lib' },
needs: ['build'],
},
snapshotGate(),
pnpmScript('publint', 'publint', { needs: ['build'] }),
pnpmScript('node-next-types', 'verify-node-next-types', {
label: 'node-next types',
needs: ['build'],
}),
builtPackageInvariantsGate(['build']),
builtBinSmokeGate(),
]
}
function lintGate(eslintTargets: readonly string[] = ['.']): Gate {
const concurrencyArgs = eslintConcurrencyArgs()
if (process.env.DSH_ESLINT_CACHE === '1') {
return pnpmExec('lint', [
'eslint',
'.',
...eslintTargets,
...concurrencyArgs,
'--cache',
'--cache-location',
'.cache/eslint/',
@@ -280,11 +349,28 @@ function lintGate(): Gate {
env: { NODE_OPTIONS: nodeOptions('--max-old-space-size=8192') },
})
}
if (concurrencyArgs.length > 0) {
return pnpmExec('lint', ['eslint', ...eslintTargets, ...concurrencyArgs], {
label: 'lint',
env: { NODE_OPTIONS: nodeOptions('--max-old-space-size=8192') },
})
}
return pnpmScript('lint', 'lint', {
env: { NODE_OPTIONS: nodeOptions('--max-old-space-size=8192') },
})
}
function eslintConcurrencyArgs(): string[] {
const raw = process.env.DSH_ESLINT_CONCURRENCY
if (raw === undefined || raw === '') return []
if (raw === 'auto') return ['--concurrency=auto']
const parsed = Number.parseInt(raw, 10)
if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) {
throw new Error(`run-gates: DSH_ESLINT_CONCURRENCY must be a positive integer or auto, got ${JSON.stringify(raw)}.`)
}
return [`--concurrency=${raw}`]
}
function coverageGate(): Gate {
return pnpmExec('coverage', [
'vitest',
@@ -293,8 +379,6 @@ function coverageGate(): Gate {
...positiveIntArg('DSH_COVERAGE_MAX_WORKERS', '--maxWorkers'),
], {
label: 'test:coverage',
env: { DSH_EXAMPLE_MODE: 'lib' },
needs: ['build'],
})
}
@@ -325,6 +409,13 @@ function positiveIntArg(envName: string, flag: string): string[] {
return [`${flag}=${raw}`]
}
function flagEnabled(envName: string): boolean {
const raw = process.env[envName]
if (raw === undefined || raw === '') return false
if (raw !== '1') throw new Error(`run-gates: ${envName} must be 1 when set, got ${JSON.stringify(raw)}.`)
return true
}
function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] {
const artifactOptions = options.artifactNeeds === undefined ? {} : { needs: options.artifactNeeds }
return [
@@ -343,6 +434,7 @@ function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] {
function docSyncLeafGates(options: {
docTypecheckNeeds?: string[]
docTypecheckEnv?: Record<string, string | undefined>
docsBuildScript?: 'docs:build' | 'docs:build:mpa'
} = {}): Gate[] {
const docTypecheckOptions: Partial<Gate> = {}
if (options.docTypecheckNeeds !== undefined) docTypecheckOptions.needs = options.docTypecheckNeeds
@@ -369,8 +461,11 @@ function docSyncLeafGates(options: {
pnpmScript('translation-prompt', 'verify-translation-prompt', { label: 'translation prompt' }),
pnpmScript('translation-pairing', 'verify-translation-pairing', { label: 'translation pairing' }),
pnpmScript('doc-budgets', 'verify-doc-budgets', { label: 'doc budgets' }),
// Keep the VitePress build in this single gate because projection rewrites website/.generated.
pnpmScript('docs-site', 'docs:check', { label: 'documentation site' }),
pnpmExec('docs-site-projection', ['vitest', 'run', 'scripts/project-doc-site.spec.ts'], {
label: 'documentation projection',
}),
// Keep the VitePress build itself in one gate because projection rewrites website/.generated.
pnpmScript('docs-site-build', options.docsBuildScript ?? 'docs:build', { label: 'documentation build' }),
pnpmScript('package-readme-limitations', 'verify-package-readme-limitations', { label: 'package README limitations' }),
]
}
@@ -540,7 +635,8 @@ function printSummary(results: GateResult[], durationMs: number): void {
for (const result of unsuccessful) {
const duration = (result.durationMs / 1000).toFixed(2)
const reason = result.error ?? (result.exitCode === null ? 'no exit code' : `exit ${result.exitCode}`)
console.error(` - ${result.status.toUpperCase()} ${result.gate.label} (${duration}s, ${reason})`)
const disposition = result.gate.allowFailure === true ? 'NON-BLOCKING ' : ''
console.error(` - ${disposition}${result.status.toUpperCase()} ${result.gate.label} (${duration}s, ${reason})`)
console.error(` ${result.gate.displayCommand}`)
}
}

View File

@@ -1,102 +1,106 @@
/** Verify every packed companion through its package self-reference under plain Node. */
/** Verify every compiled companion through its staged package self-reference under plain Node. */
import { spawnSync } from 'node:child_process'
import {
copyFileSync,
cpSync,
existsSync,
globSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs'
import { dirname, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
const root = resolve(import.meta.dirname, '..')
const loaderUrl = pathToFileURL(resolve(root, 'vendor/loader/lib/index.js')).href
const repositoryRoot = resolve(import.meta.dirname, '..')
const options = parseOptions(process.argv.slice(2))
const packagesRoot = resolve(options.get('--packages-root') ?? repositoryRoot)
const loaderUrl = options.get('--loader-url')
?? pathToFileURL(resolve(repositoryRoot, 'vendor/loader/lib/index.js')).href
const failures = []
const manifests = globSync('packages/*/*/package.json', { cwd: root }).sort()
const packArgs = ['pack', '--dry-run', '--json', '--ignore-scripts']
// Windows cannot spawn npm's .cmd shim directly; setup-node installs this JS
// entrypoint beside node.exe, so the probe stays shell-free on every runner.
const npmInvocation = process.platform === 'win32'
? [process.execPath, [resolve(dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js'), ...packArgs]]
: ['npm', packArgs]
const manifests = globSync('packages/*/*/package.json', { cwd: packagesRoot }).sort()
const { default: Loader } = await import(loaderUrl)
const loader = Object.create(Loader.prototype)
for (const manifestPath of manifests) {
const packageDir = dirname(resolve(root, manifestPath))
const manifest = JSON.parse(readFileSync(resolve(root, manifestPath), 'utf8'))
const packageDir = dirname(resolve(packagesRoot, manifestPath))
const manifest = JSON.parse(readFileSync(resolve(packagesRoot, manifestPath), 'utf8'))
const packageName = manifest.name
if (typeof packageName !== 'string' || packageName.length === 0) {
failures.push(`${manifestPath}: missing package name`)
continue
}
const pack = spawnSync(npmInvocation[0], npmInvocation[1], {
cwd: packageDir,
encoding: 'utf8',
})
if (pack.status !== 0) {
const detail = pack.error?.message
?? (pack.stderr.trim() || pack.stdout.trim() || `npm pack exited ${pack.status}`)
failures.push(`${packageName}: ${detail}`)
const invariantExport = manifest.exports?.['./invariant']
if (typeof invariantExport !== 'object'
|| invariantExport.default !== './lib/invariant.js'
|| !manifest.files?.includes('lib/invariant.js')) {
failures.push(`${packageName}: manifest does not publish ./lib/invariant.js as ./invariant`)
continue
}
let files
try {
const result = JSON.parse(pack.stdout)
files = result[0]?.files
if (!Array.isArray(files)) throw new Error('npm pack returned no file inventory')
} catch (error) {
failures.push(`${packageName}: cannot parse npm pack inventory: ${String(error)}`)
continue
}
// Keep the packed view below its owning package so Node reaches the real
// Keep the staged view below its owning package so Node reaches the real
// pnpm dependency links. Junctioning node_modules elsewhere breaks pnpm's
// relative workspace links on Windows.
const stagedPackageDir = mkdtempSync(resolve(packageDir, '.dsh-packed-invariant-'))
// relative workspace links on Windows. Copy the manifest-declared lib view
// so a companion that imports an undeclared runtime chunk fails here.
const stagedPackageDir = mkdtempSync(resolve(packageDir, '.dsh-built-invariant-'))
try {
for (const file of files) {
if (typeof file.path !== 'string'
|| (file.path !== 'package.json' && !file.path.startsWith('lib/'))) continue
const target = resolve(stagedPackageDir, file.path)
mkdirSync(dirname(target), { recursive: true })
copyFileSync(resolve(packageDir, file.path), target)
}
const probe = `
const companion = await import(${JSON.stringify(`${packageName}/invariant`)});
const { default: Loader } = await import(${JSON.stringify(loaderUrl)});
if ('default' in companion) throw new Error('companion has a default export');
const loader = Object.create(Loader.prototype);
const unwrapped = loader.unwrapExports(companion);
if (unwrapped !== companion) throw new Error('Loader collapsed the companion namespace');
if (typeof unwrapped.name !== 'string') throw new Error('companion name is missing');
if (!Array.isArray(unwrapped.inject) || !unwrapped.inject.includes('invariants')) {
throw new Error('companion does not inject invariants');
}
if (typeof unwrapped.apply !== 'function') throw new Error('companion apply is missing');
`
const result = spawnSync(process.execPath, ['--input-type=module', '--eval', probe], {
cwd: stagedPackageDir,
encoding: 'utf8',
})
if (result.status !== 0) {
const detail = result.error?.message
?? (result.stderr.trim() || result.stdout.trim() || `node exited ${result.status}`)
failures.push(`${packageName}: ${detail}`)
copyFileSync(resolve(packageDir, 'package.json'), resolve(stagedPackageDir, 'package.json'))
copyDeclaredLibFiles(packageDir, stagedPackageDir, manifest.files)
const probePath = resolve(stagedPackageDir, 'probe.mjs')
writeFileSync(
probePath,
`import * as companion from ${JSON.stringify(`${packageName}/invariant`)}\nexport default companion\n`,
)
const { default: companion } = await import(pathToFileURL(probePath).href)
if ('default' in companion) throw new Error('companion has a default export')
const unwrapped = loader.unwrapExports(companion)
if (unwrapped !== companion) throw new Error('Loader collapsed the companion namespace')
if (typeof unwrapped.name !== 'string') throw new Error('companion name is missing')
if (!Array.isArray(unwrapped.inject) || !unwrapped.inject.includes('invariants')) {
throw new Error('companion does not inject invariants')
}
if (typeof unwrapped.apply !== 'function') throw new Error('companion apply is missing')
} catch (error) {
failures.push(`${packageName}: ${error instanceof Error ? error.message : String(error)}`)
} finally {
rmSync(stagedPackageDir, { recursive: true, force: true })
}
}
if (failures.length > 0) {
console.error('verify-built-package-invariants: packed companion failures:')
console.error('verify-built-package-invariants: compiled companion failures:')
for (const failure of failures) console.error(` ${failure}`)
process.exit(1)
}
console.log(`verify-built-package-invariants: ${manifests.length} packed companion(s) passed plain-Node Loader checks.`)
console.log(`verify-built-package-invariants: ${manifests.length} compiled companion(s) passed plain-Node Loader checks.`)
function parseOptions(args) {
const allowed = new Set(['--packages-root', '--loader-url'])
const parsed = new Map()
for (let index = 0; index < args.length; index += 2) {
const name = args[index]
const value = args[index + 1]
if (!allowed.has(name) || value === undefined || value.startsWith('--')) {
throw new Error(`verify-built-package-invariants: expected [--packages-root PATH] [--loader-url URL], got ${JSON.stringify(args)}.`)
}
if (parsed.has(name)) throw new Error(`verify-built-package-invariants: duplicate option ${name}.`)
parsed.set(name, value)
}
return parsed
}
function copyDeclaredLibFiles(packageDir, stagedPackageDir, files) {
for (const pattern of files) {
if (!pattern.startsWith('lib/')) continue
for (const relativePath of globSync(pattern, { cwd: packageDir })) {
const source = resolve(packageDir, relativePath)
if (!existsSync(source)) continue
const target = resolve(stagedPackageDir, relativePath)
mkdirSync(dirname(target), { recursive: true })
cpSync(source, target, { recursive: true })
}
}
}

View File

@@ -0,0 +1,88 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { spawnSync } from 'node:child_process'
import { afterEach, describe, expect, it } from 'vitest'
const verifier = fileURLToPath(new URL('./verify-built-package-invariants.mjs', import.meta.url))
const roots: string[] = []
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
})
function fixture(options: {
invariantSource?: string
invariantExport?: string
runtimeChunk?: string
} = {}): { root: string; loaderUrl: string } {
const root = mkdtempSync(join(tmpdir(), 'dsh-built-package-invariants-'))
roots.push(root)
const packageDir = join(root, 'packages/core/probe')
mkdirSync(join(packageDir, 'lib'), { recursive: true })
writeFileSync(join(packageDir, 'package.json'), `${JSON.stringify({
name: '@deepseek-ai/dsh-probe',
type: 'module',
files: ['lib/invariant.js'],
exports: {
'./invariant': {
default: options.invariantExport ?? './lib/invariant.js',
},
},
}, null, 2)}\n`)
writeFileSync(
join(packageDir, 'lib/invariant.js'),
options.invariantSource ?? "export const name = 'probe-invariant'\nexport const inject = ['invariants']\nexport const apply = () => {}\n",
)
if (options.runtimeChunk !== undefined) {
writeFileSync(join(packageDir, 'lib/chunk.js'), options.runtimeChunk)
}
const loaderPath = join(root, 'loader.mjs')
writeFileSync(loaderPath, 'export default class Loader { unwrapExports(value) { return value } }\n')
return { root, loaderUrl: pathToFileURL(loaderPath).href }
}
function verify(root: string, loaderUrl: string) {
return spawnSync(process.execPath, [
verifier,
'--packages-root', root,
'--loader-url', loaderUrl,
], {
encoding: 'utf8',
timeout: 5_000,
})
}
describe('built package invariant verifier', () => {
it('loads the staged compiled self-reference through plain Node and Loader normalization', () => {
const { root, loaderUrl } = fixture()
const result = verify(root, loaderUrl)
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain('1 compiled companion(s) passed plain-Node Loader checks')
})
it('rejects a default export and a broken invariant export map', () => {
const withDefault = fixture({
invariantSource: "export default {}\nexport const name = 'probe-invariant'\nexport const inject = ['invariants']\nexport const apply = () => {}\n",
})
const defaultResult = verify(withDefault.root, withDefault.loaderUrl)
expect(defaultResult.status).toBe(1)
expect(defaultResult.stderr).toContain('companion has a default export')
const brokenExport = fixture({ invariantExport: './lib/missing.js' })
const exportResult = verify(brokenExport.root, brokenExport.loaderUrl)
expect(exportResult.status).toBe(1)
expect(exportResult.stderr).toContain('@deepseek-ai/dsh-probe')
})
it('rejects an invariant bundle that needs an unstaged runtime chunk', () => {
const { root, loaderUrl } = fixture({
invariantSource: "export * from './chunk.js'\n",
runtimeChunk: "export const name = 'probe-invariant'\nexport const inject = ['invariants']\nexport const apply = () => {}\n",
})
const result = verify(root, loaderUrl)
expect(result.status).toBe(1)
expect(result.stderr).toContain('chunk.js')
})
})