Files
MailUI4Agents/plugins/zcode-mail-bridge/test/is-main.test.mjs
JianFeeeee 6a7356ebe7 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;坏路径能被握手判据发现(反向对照)。
2026-09-12 15:06:49 +08:00

70 lines
2.8 KiB
JavaScript
Raw 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.

/**
* 入口判断的测试 —— 这个缺陷只在**部署形态**下出现,所以必须专门钉住。
*
* 实测经过:生产布局是 `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);
});