fix(zcode): 软链部署下入口静默不执行 + 部署脚本支持 zcode 快照
## 缺陷:入口判断不解析软链 → 生产形态下 main() 从不执行
`mcp/server.mjs` 与 `src/index.mjs` 都这么判断是否被直接执行:
import.meta.url === `file://${process.argv[1]}`
而 ESM 的 `import.meta.url` 是**解析过软链的真实路径**,argv 是命令行里写的那个。
生产布局是软链(`/opt/agentmail/plugins/<name>/current` → 时间戳目录),
于是两者不等、`main()` 从不执行:**没有输出、没有报错、退出码 0**。
它是被部署脚本的后置验证抓到的:第一次从快照起 MCP 服务器时
「握手 0 个工具、stderr 一个字都没有」,而同一个文件从仓库路径跑完全正常
(仓库路径没有软链)。这类缺陷只在部署形态下出现,本地怎么试都对;
表现(静默成功)又与「功能没被调用」一模一样。
修法:新增 `lib/is-main.mjs`,**两侧都 realpath** 后比较(只归一 != 「同一文件」)。
单测含目录软链、文件软链、文件不存在(保守判否,避免被 import 时误跑一遍)。
## 部署脚本:引入 HOST 概念,四个插件一条部署路径
pi/opencode/dsh 的宿主是 systemd 单元,zcode 的宿主是 ZCode 应用本身 ——
没有我们的单元可重启。于是:
- `HOST=systemd`:重启单元 + 看网关库里有没有新心跳(原有判据)
- `HOST=zcode`:**从快照起一次 MCP 服务器并走完握手**,这与 ZCode 加载插件
走的是同一份入口代码;另查 `plugins list` 报告的路径是不是 current
后置验证带**判据自检**:握手函数对坏路径必须返回 0,否则判据本身失效就拒绝通过。
计数用 `grep -o | wc -l` 而不是 `grep -c` —— 每条响应只占一行,tools/list 的
11 个工具名全在同一行,用 -c 会得到 2,把好快照判失败(实测踩过)。
## 生产已切到快照
`/opt/agentmail/plugins/zcode-mail-bridge/current → 20260912-150547`,
`~/.zcode/cli/config.json` 的 `plugins.dirs` 已指向 current,
`zcode plugins list` 报的路径就是快照路径。仓库不再是生产代码。
验证:单测 325/325;快照握手 12 个 name 字段;官方 __zcode-plugin-host 从快照
启动正常;钩子从快照跑通 409 → block;坏路径能被握手判据发现(反向对照)。
This commit is contained in:
@ -43,11 +43,11 @@ for arg in "$@"; do
|
|||||||
case "$arg" in
|
case "$arg" in
|
||||||
-h|--help) usage; exit 0 ;;
|
-h|--help) usage; exit 0 ;;
|
||||||
--stage-only) STAGE_ONLY=1 ;;
|
--stage-only) STAGE_ONLY=1 ;;
|
||||||
pi|opencode|dsh) PLUGIN="$arg" ;;
|
pi|opencode|dsh|zcode) PLUGIN="$arg" ;;
|
||||||
*) echo "未知参数: $arg" >&2; exit 2 ;;
|
*) echo "未知参数: $arg" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
[ -n "$PLUGIN" ] || { echo "用法: $0 <pi|opencode|dsh> [--stage-only]" >&2; exit 2; }
|
[ -n "$PLUGIN" ] || { echo "用法: $0 <pi|opencode|dsh|zcode> [--stage-only]" >&2; exit 2; }
|
||||||
|
|
||||||
say() { printf '\n=== %s\n' "$*"; }
|
say() { printf '\n=== %s\n' "$*"; }
|
||||||
ok() { printf ' [ OK ] %s\n' "$*"; }
|
ok() { printf ' [ OK ] %s\n' "$*"; }
|
||||||
@ -61,11 +61,18 @@ DEST="$DEST_ROOT/$PLUGIN-mail-bridge"
|
|||||||
SNAP="$DEST/$TS"
|
SNAP="$DEST/$TS"
|
||||||
STAGING="$DEST/.$TS.staging"
|
STAGING="$DEST/.$TS.staging"
|
||||||
|
|
||||||
# 每个桥的差异集中在这张表里:入口、systemd 单元(opencode/dsh 不是独立 unit)。
|
# 每个桥的差异集中在这张表里:入口、宿主、构建需求。
|
||||||
|
#
|
||||||
|
# HOST 决定「切换之后拿什么判活」,两者完全不同:
|
||||||
|
# systemd → 重启单元 + 看网关库里有没有新心跳
|
||||||
|
# zcode → **没有我们的单元可重启**:ZCode 是宿主,它在会话开始时读
|
||||||
|
# `plugins.dirs`。于是判活改成「从快照起 MCP 服务器并走一遍握手」——
|
||||||
|
# 这与「宿主加载插件」走的是同一份入口代码。
|
||||||
case "$PLUGIN" in
|
case "$PLUGIN" in
|
||||||
pi) ENTRY="src/index.mjs"; UNIT="pi-mail-bridge"; NEEDS_BUILD=0 ;;
|
pi) ENTRY="src/index.mjs"; UNIT="pi-mail-bridge"; HOST="systemd"; NEEDS_BUILD=0 ;;
|
||||||
opencode) ENTRY="index.js"; UNIT="opencode-serve"; NEEDS_BUILD=0 ;;
|
opencode) ENTRY="index.js"; UNIT="opencode-serve"; HOST="systemd"; NEEDS_BUILD=0 ;;
|
||||||
dsh) ENTRY="dist/index.js"; UNIT="dsh"; NEEDS_BUILD=1 ;;
|
dsh) ENTRY="dist/index.js"; UNIT="dsh"; HOST="systemd"; NEEDS_BUILD=1 ;;
|
||||||
|
zcode) ENTRY="src/index.mjs"; UNIT=""; HOST="zcode"; NEEDS_BUILD=0 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
say "部署 $PLUGIN-mail-bridge → $SNAP"
|
say "部署 $PLUGIN-mail-bridge → $SNAP"
|
||||||
@ -73,10 +80,17 @@ say "部署 $PLUGIN-mail-bridge → $SNAP"
|
|||||||
# ── 1. 前置断言 ────────────────────────────────────────────────
|
# ── 1. 前置断言 ────────────────────────────────────────────────
|
||||||
[ -d "$SRC" ] || { bad "源目录不存在: $SRC"; exit 2; }
|
[ -d "$SRC" ] || { bad "源目录不存在: $SRC"; exit 2; }
|
||||||
[ -f "$SRC/package.json" ] || { bad "缺少 package.json: $SRC"; exit 2; }
|
[ -f "$SRC/package.json" ] || { bad "缺少 package.json: $SRC"; exit 2; }
|
||||||
command -v systemctl >/dev/null 2>&1 || { bad "缺少 systemctl"; exit 2; }
|
if [ "$HOST" = "systemd" ]; then
|
||||||
[ -f "$GATEWAY_DB" ] || { bad "找不到网关库 $GATEWAY_DB(无法验证桥是否连上)"; exit 2; }
|
command -v systemctl >/dev/null 2>&1 || { bad "缺少 systemctl"; exit 2; }
|
||||||
if ! systemctl cat "$UNIT" >/dev/null 2>&1; then
|
[ -f "$GATEWAY_DB" ] || { bad "找不到网关库 $GATEWAY_DB(无法验证桥是否连上)"; exit 2; }
|
||||||
bad "找不到 systemd 单元 $UNIT(插件要先有一个运行宿主才能谈部署)"; exit 2
|
if ! systemctl cat "$UNIT" >/dev/null 2>&1; then
|
||||||
|
bad "找不到 systemd 单元 $UNIT(插件要先有一个运行宿主才能谈部署)"; exit 2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# zcode 的宿主是应用本身:要能跑 CLI 才能自查「插件被发现了吗」。
|
||||||
|
ZCODE_CLI=${ZCODE_CLI:-/opt/ZCode/resources/glm/zcode.cjs}
|
||||||
|
[ -f "$ZCODE_CLI" ] || { bad "找不到 ZCode CLI: $ZCODE_CLI"; exit 2; }
|
||||||
|
command -v node >/dev/null 2>&1 || { bad "缺少 node"; exit 2; }
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$NEEDS_BUILD" = 1 ]; then
|
if [ "$NEEDS_BUILD" = 1 ]; then
|
||||||
@ -154,7 +168,7 @@ if [ "$STAGE_ONLY" = 1 ]; then
|
|||||||
# 干跑:把快照留在 .staging,**不切 current、不重启**。
|
# 干跑:把快照留在 .staging,**不切 current、不重启**。
|
||||||
say "干跑结束(--stage-only)"
|
say "干跑结束(--stage-only)"
|
||||||
info "快照留在: $STAGING"
|
info "快照留在: $STAGING"
|
||||||
info "未做: 原子切换 / 重启 $UNIT / 后置验证"
|
info "未做: 原子切换${UNIT:+ / 重启 $UNIT} / 后置验证"
|
||||||
info "要真部署:$0 $PLUGIN"
|
info "要真部署:$0 $PLUGIN"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@ -170,13 +184,74 @@ ok "current → $TS${PREV:+(上一版 $PREV)}"
|
|||||||
rollback() {
|
rollback() {
|
||||||
if [ -n "$PREV" ] && [ -d "$DEST/$PREV" ]; then
|
if [ -n "$PREV" ] && [ -d "$DEST/$PREV" ]; then
|
||||||
ln -sfn "$PREV" "$DEST/current"
|
ln -sfn "$PREV" "$DEST/current"
|
||||||
systemctl restart "$UNIT" >/dev/null 2>&1
|
[ -n "$UNIT" ] && systemctl restart "$UNIT" >/dev/null 2>&1
|
||||||
warn "已回滚到 $PREV 并重启 $UNIT"
|
warn "已回滚到 $PREV${UNIT:+ 并重启 $UNIT}"
|
||||||
else
|
else
|
||||||
warn "无上一版可回滚(这是首次部署)—— 请手工处理"
|
warn "无上一版可回滚(这是首次部署)—— 请手工处理"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── 5b. zcode:宿主不是我们的 unit,判活方式不同 ──────────────
|
||||||
|
# 判据:**从快照里起一次 MCP 服务器并走完握手**。
|
||||||
|
# 这与 ZCode 加载插件走的是同一份入口代码,所以能发现「快照缺文件」
|
||||||
|
# 「相对导入断了」「清单被改坏」这类只有加载时才暴露的问题。
|
||||||
|
# 检查系统 MCP 握手(stdin 进、stdout 出),返回响应里的 name 字段个数(失败回声 0)。
|
||||||
|
#
|
||||||
|
# 计数必须用 `grep -o | wc -l`(数**次数**),不能用 `grep -c`(数**行**):
|
||||||
|
# 每一条响应只占一行,tools/list 的 11 个工具名全在同一行里,
|
||||||
|
# 用 -c 会得到 2(就那么两行),于是好快照也会被判失败(实测踩过)。
|
||||||
|
# 期望值:11 个工具 + initialize 里的 serverInfo.name = 12。
|
||||||
|
mcp_handshake() {
|
||||||
|
local root="$1"
|
||||||
|
printf '%s\n' \
|
||||||
|
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05"}}' \
|
||||||
|
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
|
||||||
|
| AGENTMAIL_AGENT_NAME=probe timeout 30 node "$root/mcp/server.mjs" 2>/dev/null \
|
||||||
|
| grep -o '"name":"' | wc -l | tr -d ' ' || true
|
||||||
|
}
|
||||||
|
MIN_NAMES=11
|
||||||
|
|
||||||
|
if [ "$HOST" = "zcode" ]; then
|
||||||
|
say "后置验证(zcode:宿主是应用,不能重启它)"
|
||||||
|
TOOLS=$(mcp_handshake "$DEST/current")
|
||||||
|
if [ "${TOOLS:-0}" -ge "$MIN_NAMES" ]; then
|
||||||
|
ok "从快照起的 MCP 服务器握手成功,报出 $TOOLS 个 name 字段"
|
||||||
|
else
|
||||||
|
bad "快照里的 MCP 服务器握手失败(只报出 ${TOOLS:-0} 个 name 字段,期望 ≥ $MIN_NAMES)"
|
||||||
|
rollback
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# 反向对照:握手判据必须能发现坏快照,否则「全绿」没有意义。
|
||||||
|
if [ "$(mcp_handshake "$DEST/__definitely_missing__")" -ge "$MIN_NAMES" ]; then
|
||||||
|
bad "握手判据无法发现坏路径 —— 判据本身失效,拒绝通过"
|
||||||
|
rollback
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ok "握手判据自检通过(坏路径能被发现)"
|
||||||
|
|
||||||
|
# ZCode 从 plugins.dirs 发现插件;没有指向 current 就还没真的切换。
|
||||||
|
LIST=$(timeout 60 node "$ZCODE_CLI" plugins list 2>/dev/null || true)
|
||||||
|
if printf '%s' "$LIST" | grep -q "agentmail@inline"; then
|
||||||
|
WHERE=$(printf '%s' "$LIST" | grep -A 1 'agentmail@inline' | grep -o '/opt/[^ ]*' | head -1)
|
||||||
|
if [ "$WHERE" = "$DEST/current" ]; then
|
||||||
|
ok "ZCode 已从快照发现插件($WHERE)"
|
||||||
|
else
|
||||||
|
warn "ZCode 已发现 agentmail,但路径是 $WHERE(不是 $DEST/current)"
|
||||||
|
info "改这一处即可:~/.zcode/cli/config.json 的 plugins.dirs"
|
||||||
|
info " \"dirs\": [\"$DEST/current\"]"
|
||||||
|
info "改完要让 ZCode 重新读取(它会话开始时读;重启应用最保险)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "ZCode 还没发现 agentmail 插件(plugins.dirs 尚未指向 current?)"
|
||||||
|
info " \"dirs\": [\"$DEST/current\"]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
say "结论: 快照已切换"
|
||||||
|
info "运行路径: $DEST/current/$ENTRY"
|
||||||
|
info "回滚命令: ln -sfn '${PREV:-$TS}' '$DEST/current'"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# ── 6. 重启 + 后置验证 ─────────────────────────────────────────
|
# ── 6. 重启 + 后置验证 ─────────────────────────────────────────
|
||||||
# 记下重启时刻,后面用「网关库里的 last_seen 是否比它新」判断桥真的连上了。
|
# 记下重启时刻,后面用「网关库里的 last_seen 是否比它新」判断桥真的连上了。
|
||||||
# **必须用 UTC。** `agents.last_seen` 是 UTC(SQLite 的 CURRENT_TIMESTAMP 语义),
|
# **必须用 UTC。** `agents.last_seen` 是 UTC(SQLite 的 CURRENT_TIMESTAMP 语义),
|
||||||
|
|||||||
42
plugins/zcode-mail-bridge/lib/is-main.mjs
Normal file
42
plugins/zcode-mail-bridge/lib/is-main.mjs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* 「这个模块是不是被直接执行的入口?」——**必须比真实路径**。
|
||||||
|
*
|
||||||
|
* # 为什么不能直接比 `import.meta.url === 'file://' + process.argv[1]`
|
||||||
|
*
|
||||||
|
* ESM 的 `import.meta.url` 已经是**解析过软链的真实路径**,而 `process.argv[1]`
|
||||||
|
* 是命令行里写的那个(可能是软链)。两者在软链下不相等,于是入口判断为假、
|
||||||
|
* `main()` 从不执行 —— 没有输出、没有报错、**退出码 0**。
|
||||||
|
*
|
||||||
|
* 这不是假设:生产部署的布局就是软链(`/opt/agentmail/plugins/<name>/current`
|
||||||
|
* → 某个时间戳目录),第一次从快照起 MCP 服务器时正是这样「握手 0 个工具、
|
||||||
|
* stderr 一个字都没有」。而同一个文件从仓库路径跑是正常的 ——
|
||||||
|
* 因为仓库路径没有软链。
|
||||||
|
*
|
||||||
|
* 这类缺陷的特点是**只在部署形态下出现**,本地怎么试都是对的;
|
||||||
|
* 而它的表现(静默成功)与「功能没被调用」完全一样。
|
||||||
|
*
|
||||||
|
* # 为什么两边都要 realpath
|
||||||
|
*
|
||||||
|
* 只解析一侧仍会在「argv 是软链、真身是另一个软链」这类组合下失配。
|
||||||
|
* 两侧都归一到真实路径,判断才是「同一个文件」这个语义本身。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { realpathSync } from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} metaUrl 调用方传 `import.meta.url`
|
||||||
|
* @param {string} [argv1] 默认 `process.argv[1]`
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isMainModule(metaUrl, argv1 = process.argv[1]) {
|
||||||
|
if (!metaUrl || !argv1) return false;
|
||||||
|
try {
|
||||||
|
return realpathSync(fileURLToPath(metaUrl)) === realpathSync(argv1);
|
||||||
|
} catch {
|
||||||
|
// 文件不存在 / 权限不足:**当作不是**入口(保守)。
|
||||||
|
// 判错的代价:要么该跑的没跑(静默),要么被 import 时误跑一遍。
|
||||||
|
// 后者会带来副作用(起服务、改状态),更危险。
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -25,6 +25,7 @@ import { createInterface } from 'node:readline';
|
|||||||
import { GatewayClient } from '../lib/gateway.mjs';
|
import { GatewayClient } from '../lib/gateway.mjs';
|
||||||
import { buildTools, indexTools } from '../lib/tools.mjs';
|
import { buildTools, indexTools } from '../lib/tools.mjs';
|
||||||
import { handleLine, SERVER_NAME, SERVER_VERSION } from '../lib/mcp-rpc.mjs';
|
import { handleLine, SERVER_NAME, SERVER_VERSION } from '../lib/mcp-rpc.mjs';
|
||||||
|
import { isMainModule } from '../lib/is-main.mjs';
|
||||||
|
|
||||||
const log = (...parts) => console.error('[agentmail-mcp]', ...parts);
|
const log = (...parts) => console.error('[agentmail-mcp]', ...parts);
|
||||||
|
|
||||||
@ -92,9 +93,12 @@ export async function main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直接执行时启动;被 import 时只导出(便于测试与宿主按需调用)。
|
// 直接执行时启动;被 import 时只导出。
|
||||||
const isDirect = process.argv[1] && import.meta.url === `file://${process.argv[1]}`;
|
//
|
||||||
if (isDirect) {
|
// 判断**必须解析软链**(见 lib/is-main.mjs):生产布局是 `current` 软链,
|
||||||
|
// 直接比 `import.meta.url === 'file://'+argv[1]` 会判假 —— 服务器什么都不做、
|
||||||
|
// 无输出、退出码 0(实测)。
|
||||||
|
if (isMainModule(import.meta.url)) {
|
||||||
main().catch(error => {
|
main().catch(error => {
|
||||||
log('致命错误:', error?.stack || error);
|
log('致命错误:', error?.stack || error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@ -46,6 +46,7 @@ import { explicitSendsFile, readExplicitSends } from '../lib/explicit-sends.mjs'
|
|||||||
import { zcodeModeForTier, modeReachesPermissionHook, describeTier } from './turn-mode.mjs';
|
import { zcodeModeForTier, modeReachesPermissionHook, describeTier } from './turn-mode.mjs';
|
||||||
import { buildMailPrompt, replySubject, renderTurnFailure } from './prompt.mjs';
|
import { buildMailPrompt, replySubject, renderTurnFailure } from './prompt.mjs';
|
||||||
import { runTurn, DEFAULT_CLI } from './zcode-run.mjs';
|
import { runTurn, DEFAULT_CLI } from './zcode-run.mjs';
|
||||||
|
import { isMainModule } from '../lib/is-main.mjs';
|
||||||
|
|
||||||
const log = (...parts) => console.error('[zcode-mail-bridge]', ...parts);
|
const log = (...parts) => console.error('[zcode-mail-bridge]', ...parts);
|
||||||
|
|
||||||
@ -391,8 +392,11 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 直接执行时启动;被 import 时只导出(测试要用 createDriver)。
|
// 直接执行时启动;被 import 时只导出(测试要用 createDriver)。
|
||||||
const isDirect = process.argv[1] && import.meta.url === `file://${process.argv[1]}`;
|
//
|
||||||
if (isDirect) {
|
// 判断**必须解析软链**(见 lib/is-main.mjs):生产布局是 `current` 软链,
|
||||||
|
// 直接比字符串会让驱动什么都不做就退出(退出码 0)—— 而那看起来
|
||||||
|
// 与「服务正常启动但没收到信」一模一样。
|
||||||
|
if (isMainModule(import.meta.url)) {
|
||||||
main().catch(e => {
|
main().catch(e => {
|
||||||
log(`启动失败:${describeError(e)}`);
|
log(`启动失败:${describeError(e)}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
69
plugins/zcode-mail-bridge/test/is-main.test.mjs
Normal file
69
plugins/zcode-mail-bridge/test/is-main.test.mjs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* 入口判断的测试 —— 这个缺陷只在**部署形态**下出现,所以必须专门钉住。
|
||||||
|
*
|
||||||
|
* 实测经过:生产布局是 `current` 软链,入口用
|
||||||
|
* `import.meta.url === 'file://' + process.argv[1]` 判断是否直接执行 ——
|
||||||
|
* `import.meta.url` 是解析过软链的真实路径,argv 是软链路径,两者不等,
|
||||||
|
* 于是 `main()` 从不执行:**没有输出、没有报错、退出码 0**。
|
||||||
|
*
|
||||||
|
* 从仓库路径跑(无软链)完全正常,所以本地怎么试都发现不了。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { mkdtemp, mkdir, writeFile, symlink, rm } from 'node:fs/promises';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { pathToFileURL } from 'node:url';
|
||||||
|
import { isMainModule } from '../lib/is-main.mjs';
|
||||||
|
|
||||||
|
test('同一个文件的真实路径 → 是入口', () => {
|
||||||
|
const url = pathToFileURL('/etc/hostname').href;
|
||||||
|
assert.equal(isMainModule(url, '/etc/hostname'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('★ 通过软链指向自己 → 仍是入口(生产布局就是软链)', async () => {
|
||||||
|
const dir = await mkdtemp(join(tmpdir(), 'zc-is-main-'));
|
||||||
|
try {
|
||||||
|
const real = join(dir, 'real.mjs');
|
||||||
|
const link = join(dir, 'current.mjs');
|
||||||
|
await writeFile(real, '// x\n', 'utf8');
|
||||||
|
await symlink(real, link);
|
||||||
|
const url = pathToFileURL(real).href;
|
||||||
|
assert.equal(
|
||||||
|
isMainModule(url, link),
|
||||||
|
true,
|
||||||
|
'软链路径必须被认成同一个文件 —— 否则快照部署下入口静默不执行'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await rm(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('★ 目录软链(current → <时间戳>)下的完整路径同样成立', async () => {
|
||||||
|
// 生产的软链在**目录**这一层:/opt/.../<name>/current/mcp/server.mjs
|
||||||
|
const dir = await mkdtemp(join(tmpdir(), 'zc-is-main-d-'));
|
||||||
|
try {
|
||||||
|
await mkdir(join(dir, '20260101-000000', 'mcp'), { recursive: true });
|
||||||
|
const real = join(dir, '20260101-000000', 'mcp', 'server.mjs');
|
||||||
|
await writeFile(real, '// x\n', 'utf8');
|
||||||
|
await symlink('20260101-000000', join(dir, 'current'));
|
||||||
|
assert.equal(
|
||||||
|
isMainModule(pathToFileURL(real).href, join(dir, 'current', 'mcp', 'server.mjs')),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await rm(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('★ 反向对照:别的文件不是入口', () => {
|
||||||
|
assert.equal(isMainModule(pathToFileURL('/etc/hostname').href, '/etc/hosts'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('缺失的 argv 或文件不存在 → 保守判否(不误跑一遍)', () => {
|
||||||
|
assert.equal(isMainModule(pathToFileURL('/etc/hostname').href, ''), false);
|
||||||
|
assert.equal(isMainModule(pathToFileURL('/etc/hostname').href, undefined), false);
|
||||||
|
assert.equal(isMainModule(pathToFileURL('/etc/hostname').href, '/no/such/file/xyz'), false);
|
||||||
|
assert.equal(isMainModule('', '/etc/hostname'), false);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user