Files
apes-sidebar/scripts/e2e-common.sh
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

200 lines
9.6 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# scripts/e2e-common.sh — e2e-mount.sh / e2e-aggregate-mount.sh 的共享骨架:
# 日志三件套、前置校验、DSH_CMD / tarball 解析、scratch home 与清理 trap、
# scratch profile 三件套、dsh web 后台启动与就绪轮询。
#
# 纯开发侧库文件,不进 npm 包(package.json files 白名单无它);
# install.sh / install.ps1 随 npm 包独立分发,必须自包含,绝不 source 本文件。
#
# 用法:调用方先设 E2E_TAG(say/warn/die 的日志前缀)再 source:
# E2E_TAG=e2e-mount
# source "$SCRIPT_DIR/e2e-common.sh"
#
# 依赖调用方已 `set -euo pipefail` 并定义 ROOT(仓库根绝对路径)。
# 两侧脚本有意保留的差异(行为等价,靠参数 / 调用点区分):
# - DSH_HOME 子路径:mount 用 $SCRATCH/home,aggregate 直接用 $SCRATCH;
# - 就绪行 grep 正则:mount 要带 token 的完整 URL,aggregate 只取 origin
# (两侧的理由见各自脚本的就绪行注释);
# - 额外清理项:aggregate 的 fixture 打包 TMP_DIR(mount 不设即跳过)。
# =============================================================================
: "${E2E_TAG:?必须先设置 E2E_TAG(日志前缀)再 source e2e-common.sh}"
# ── 日志三件套 ───────────────────────────────────────────────────────────────
say() { printf '\033[32m[%s]\033[0m %s\n' "$E2E_TAG" "$*"; }
warn() { printf '\033[33m[%s]\033[0m %s\n' "$E2E_TAG" "$*" >&2; }
die() { printf '\033[31m[%s]\033[0m %s\n' "$E2E_TAG" "$*" >&2; exit 1; }
# ── 前置校验 ─────────────────────────────────────────────────────────────────
# e2e_require_cmd <cmd> [用途]:不在 PATH 上即 die(沿用两脚本原有报错文案)。
e2e_require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
if [ $# -ge 2 ]; then
die "未找到 $1($2)"
else
die "未找到 $1"
fi
fi
}
# ── DSH_CMD 解析 ─────────────────────────────────────────────────────────────
# PATH 上的 dsh 优先,否则 npx 拉官方包(同 scripts/install.sh)。DSH_CMD
# 缺省值(`dsh`)由调用方从环境变量取好传入。
#
# 回退的 npx 版本是显式钉死的:不钉就会解析 npm 的 `latest` dist-tag,而
# 0.1.7 线目前只有预发布(`latest` 仍是 0.1.5-rc.3),冒烟会静静地挂到一
# 个插件已不支持的宿主上。**必须写精确版本,不能写 `@alpha`**:0.1.7-rc.1
# 上线时 `alpha` 指的是 0.1.7-alpha.2,rc.1 走的是 `next`。钉版必须与
# package.json 的 peer 下限同步。
DSH_NPX_SPEC="${DSH_NPX_SPEC:-@deepseek-ai/dsh@0.1.7-rc.1}"
e2e_resolve_dsh_cmd() {
local explicit="${DSH_CMD}"
if ! command -v "$DSH_CMD" >/dev/null 2>&1; then
if command -v npx >/dev/null 2>&1; then
say "PATH 上无 ${DSH_CMD},回退 npx -y --package ${DSH_NPX_SPEC}"
DSH_CMD="npx -y --package ${DSH_NPX_SPEC} dsh"
else
die "未找到 $DSH_CMD 或 npx;请先安装 DSH CLI(npm i -g @deepseek-ai/dsh)或用 DSH_CMD 指定"
fi
fi
e2e_check_dsh_version "$explicit"
}
# 解析出的 CLI 版本必须与 peer 下限同一条支持线。这不是洁癖:宿主只比
# 基线早一个预发布时,`dsh plugin add` 的 bundle 协调会静默不写
# `dsh.profile.bundles`——CLI 退出码仍是 0,lane 却挂在「挂载未注册」上,
# 排查方向完全被带偏(本机 PATH 上的 dsh 一直是上一版,0.1.6-alpha.1 与
# 0.1.6-alpha.2 各实测过一次)。DSH_CMD 显式给出时不拦(调用方明确知道自
# 己在挂什么),只告警。
e2e_check_dsh_version() {
local explicit="$1" actual expected
expected="${DSH_EXPECT_VERSION:-0.1.7-rc.1}"
# `dsh --version` 冷启动要走 npx,给足预算;拿不到版本就只告警。
actual="$($DSH_CMD --version 2>/dev/null | tr -d '[:space:]' | head -c 64 || true)"
case "$actual" in
"$expected"|"$expected"*) return 0 ;;
esac
if [ "$explicit" = "dsh" ] && [ -n "$actual" ]; then
die "PATH 上的 dsh 是 ${actual},本仓库的基线是 ${expected}(peer 下限同一支持线)。请改用 DSH_CMD='npx -y --package @deepseek-ai/dsh@${expected} dsh',或安装该版本。"
fi
warn "DSH_CMD 版本为 ${actual:-未知},基线是 ${expected}——真机证据落在非基线宿主上。"
}
# ── tarball 解析 ─────────────────────────────────────────────────────────────
# TARBALL 已显式给出时原样使用;否则从仓库根 glob 里选。多个候选时取 mtime
# 最新——`ls | head -1` 的字典序会拿到旧版本号的历史 tarball,把冒烟挂到
# 过期产物上。未找到候选时返回非零,由调用方按各自文案 die。
e2e_resolve_tarball() {
if [ -z "${TARBALL:-}" ]; then
TARBALL="$(ls -t "$ROOT"/dsh-better-sidebar-*.tgz 2>/dev/null | head -1 || true)"
local count
count="$(ls "$ROOT"/dsh-better-sidebar-*.tgz 2>/dev/null | wc -l | tr -d ' ' || true)"
[ "$count" -le 1 ] || warn "发现 $count 个 tarball,按 mtime 选用最新:$(basename "$TARBALL")(建议清理其余)"
fi
[ -n "$TARBALL" ] && [ -f "$TARBALL" ]
}
# ── scratch home 与清理 ──────────────────────────────────────────────────────
# 始终在本调用拥有的全新目录里运行:调用方给了 DSH_HOME_BASE(可能是真实
# ~/.dsh)时,只在其下新建子目录并只删除该子目录;缺省时直接用系统临时
# 目录。调用方提供的目录本身绝不写入或删除。
# $1 = mktemp 模板短名(dsh-e2e-mount / dsh-e2e-agg,便于区分两 lane 的残留)。
# DSH_HOME 用 $SCRATCH 还是其子路径由调用方自行 export(两 lane 的既有差异)。
e2e_make_scratch() {
if [ -n "${DSH_HOME_BASE:-}" ]; then
SCRATCH="$(mktemp -d "$DSH_HOME_BASE/$1.XXXXXX")"
else
SCRATCH="$(mktemp -d /tmp/$1.XXXXXX)"
fi
}
# EXIT trap 兜底清理:杀 dsh web 后台进程;清 aggregate lane 的 fixture 打包
# 目录(TMP_DIR——mount lane 不设该变量即整段跳过);最后按 KEEP_HOME 决定
# 是否删除 scratch。结尾显式 exit 回传 trap 触发时的退出码。
e2e_cleanup() {
local code=$?
if [ -n "${SERVER_PID:-}" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
fi
if [ -n "${TMP_DIR:-}" ] && [ -d "${TMP_DIR:-}" ]; then
rm -rf "$TMP_DIR"
fi
if [ -z "${KEEP_HOME:-}" ]; then
if [ -d "${SCRATCH:-}" ]; then
rm -rf "$SCRATCH"
fi
else
warn "KEEP_HOME 已设置,保留 $SCRATCH"
fi
exit "$code"
}
# ── scratch profile 三件套 ───────────────────────────────────────────────────
# 引导 scratch profile(web 模板,镜像 dsh initProfile);先写
# pnpm-workspace.yaml 的 minimumReleaseAgeExclude,避免 pnpm 11 拒绝 <24h 新版本
# ——同 install.sh。@deepseek-ai/* 通配与仓库根 pnpm-workspace.yaml 同策:钉的
# DSH alpha 常在发布后 24h 内跑 lane,没有豁免会被 minimumReleaseAge 直接拒装。
e2e_write_profile() {
mkdir -p "$1"
cat > "$1/package.json" <<EOF
{
"name": "dsh-profile-web",
"private": true,
"dependencies": {},
"dsh": {
"profile": {
"bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"]
}
}
}
EOF
printf '[]\n' > "$1/cordis.patch.yml"
cat > "$1/pnpm-workspace.yaml" <<'EOF'
packages:
- .
nodeLinker: hoisted
autoInstallPeers: false
minimumReleaseAgeExclude:
- dsh-better-sidebar
- '@deepseek-ai/*'
EOF
}
# ── dsh web 启动 + 就绪轮询 ──────────────────────────────────────────────────
# 后台启动 dsh web(--port 0 = OS 分配,避免端口冲突;keyless 可起)。
# $1 = stdout 日志;$2 = stderr 日志(缺省合并进 $1:mount lane 单文件,
# aggregate lane 分 out/err 两个文件)。写入全局 SERVER_PID。
e2e_start_dsh_web() {
if [ $# -ge 2 ]; then
$DSH_CMD web --port "$PORT" >"$1" 2>"$2" &
else
$DSH_CMD web --port "$PORT" >"$1" 2>&1 &
fi
SERVER_PID=$!
}
# 轮询日志等待就绪行(最多 120s)。$1 = 参与轮询的日志文件;就绪行 ERE 与
# 多条命中的取舍(head/tail)由 E2E_READY_RE / E2E_READY_PICK 传入——这是
# 两条 lane 有意保留的差异(token URL vs 裸 origin,理由见各自脚本的就绪
# 行注释)。URL 提取统一为「命中串按空白分段取末段」:mount 的模式带
# `dsh web: ` 前缀(两段)故末段即完整 URL;aggregate 的模式不含空白故末段
# 即 origin——与原先两侧的 awk '{print $3}' / 直接取整行等价。
# 返回:0 = URL 已写入全局 URL;1 = 120s 超时;2 = 服务进程提前退出。
e2e_wait_dsh_web_ready() {
URL=""
local _
for _ in $(seq 1 120); do
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
return 2
fi
if URL="$(grep -oE "$E2E_READY_RE" "$1" | "${E2E_READY_PICK:-head}" -1 | awk '{print $NF}')" && [ -n "$URL" ]; then
return 0
fi
sleep 1
done
return 1
}