## 缺陷:入口判断不解析软链 → 生产形态下 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;坏路径能被握手判据发现(反向对照)。
70 lines
2.8 KiB
JavaScript
70 lines
2.8 KiB
JavaScript
/**
|
||
* 入口判断的测试 —— 这个缺陷只在**部署形态**下出现,所以必须专门钉住。
|
||
*
|
||
* 实测经过:生产布局是 `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);
|
||
});
|