Files
zcode-tooling/zcode-env-sync/tests/tools.test.mjs
T

441 lines
22 KiB
JavaScript

/**
* 新命令测试:prune / verify / doctor / status --list
* 用 USERPROFILE 重定向 HOME + 本地裸仓,不碰真实环境。
* 跑法: node --test tests/
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync, spawnSync } from "node:child_process";
import { decryptSecrets } from "../scripts/sync-core.mjs";
const SCRIPTS = path.resolve(import.meta.dirname, "..", "scripts");
const PASS = "team-passphrase-for-tests";
const GIT_ENV = { GIT_CONFIG_NOSYSTEM: "1", GIT_TERMINAL_PROMPT: "0" };
function sandboxRoot() { return fs.mkdtempSync(path.join(os.tmpdir(), "zes-tools-")); }
function makeMachine(root, name) {
const home = path.join(root, name);
for (const d of [".zcode/cli/plugins/data", ".zcode/cli/plugins/marketplaces/zcode-plugins-official",
".zcode/plugins", ".dsh/secrets", ".ssh"]) {
fs.mkdirSync(path.join(home, d), { recursive: true });
}
fs.writeFileSync(path.join(home, ".zcode/cli/plugins/marketplaces/zcode-plugins-official/marketplace.json"),
JSON.stringify({ plugins: [{ name: "browser-use", version: "0.4.2" }] }));
fs.mkdirSync(path.join(home, ".zcode/cli/plugins/data/browser-use@zcode-plugins-official"), { recursive: true });
const pdir = path.join(home, ".zcode/plugins/zcode-tps");
fs.mkdirSync(path.join(pdir, ".zcode-plugin"), { recursive: true });
fs.writeFileSync(path.join(pdir, ".zcode-plugin/plugin.json"), JSON.stringify({ name: "zcode-tps", version: "0.4.1" }));
fs.writeFileSync(path.join(pdir, "run.mjs"), "export const v=1;\n");
fs.writeFileSync(path.join(home, ".zcode/cli/config.json"),
JSON.stringify({ mcp: { servers: { s1: { type: "stdio", command: "node" } } }, plugins: { dirs: [] } }, null, 2));
fs.writeFileSync(path.join(home, ".dsh/secrets/gitea-token.txt"), `tok-${name}-${"x".repeat(28)}\n`);
fs.writeFileSync(path.join(home, ".ssh/ssh-profiles.json"), JSON.stringify({ profiles: {} }));
return home;
}
function envFor(home, extra = {}) {
const env = { ...process.env, ...GIT_ENV, USERPROFILE: home, HOME: home };
delete env.HOMEDRIVE; delete env.HOMEPATH;
delete env.ZCODE_SYNC_ALLOW_SECRETS;
return { ...env, ...extra };
}
function run(script, { home, args = [], env = {} }) {
const r = spawnSync(process.execPath, [path.join(SCRIPTS, script), ...args],
{ env: envFor(home, env), encoding: "utf8", timeout: 120000 });
return { code: r.status, out: r.stdout ?? "", err: r.stderr ?? "" };
}
function git(args, opts = {}) { return execFileSync("git", args, { encoding: "utf8", ...GIT_ENV, ...opts }); }
/** 造 N 个快照:每个改一处插件内容,保证 hash 不同、是可区分的历史 */
function seedSnapshots(home, bare, work, n) {
const env = { ZCODE_SYNC_PASSPHRASE: PASS, ZCODE_SYNC_GIT_REMOTE: bare, ZCODE_SYNC_WORK: work };
const names = [];
for (let i = 0; i < n; i++) {
fs.writeFileSync(path.join(home, ".zcode/plugins/zcode-tps/run.mjs"), `export const v=${i};\n`);
const r = run("export.mjs", { home, env });
assert.equal(r.code, 0, `第 ${i} 次导出差错:\n${r.err}`);
const latest = JSON.parse(fs.readFileSync(path.join(work, "repo/latest.json"), "utf8"));
const name = path.basename(latest.snapshot);
names.push(name);
// 同一秒内连跑会撞名 → 撞了就等一秒
if (i < n - 1) execFileSync(process.execPath, ["-e", "setTimeout(()=>{},1100)"]);
}
return names;
}
/* ---------------- prune ---------------- */
test("prune:默认预演不删,保留 keep 个 + latest 钉住", { timeout: 180000 }, () => {
const root = sandboxRoot();
const home = makeMachine(root, "m1"), bare = path.join(root, "bare.git"), work = path.join(root, "w");
git(["init", "--bare", "-b", "main", bare]);
const names = seedSnapshots(home, bare, work, 4);
const env = { ZCODE_SYNC_PASSPHRASE: PASS, ZCODE_SYNC_GIT_REMOTE: bare, ZCODE_SYNC_WORK: work };
// 预演:keep=2 → 保留最新 2 个,不删任何东西
const dry = run("prune.mjs", { home, args: ["--keep", "2"], env });
assert.equal(dry.code, 0, `预演应成功\n${dry.err}`);
assert.match(dry.out, /预演,未删除/);
assert.match(dry.out, /现有 4 个,保留 2 个,将删除 2 个/);
const snapRoot = path.join(work, "repo/snapshots");
assert.equal(fs.readdirSync(snapRoot).length, 4, "预演不得真删");
// 实删
const app = run("prune.mjs", { home, args: ["--keep", "2", "--apply"], env });
assert.equal(app.code, 0, `实删应成功\n${app.err}`);
assert.match(app.out, /实际删除 2 个/);
assert.match(app.out, /已推送/);
const left = fs.readdirSync(snapRoot);
assert.equal(left.length, 2, `应剩 2 个,实际 ${left.length}`);
// 保留的必须是最新的两个
for (const keep of names.slice(-2)) assert.ok(left.includes(keep), `应保留 ${keep}`);
// latest.json 指向的必须还在(否则后续 status/import 会找不到)
const latest = JSON.parse(fs.readFileSync(path.join(work, "repo/latest.json"), "utf8"));
assert.ok(left.includes(path.basename(latest.snapshot)), "latest 指向的快照不得被删");
// 远端也少了
git(["--git-dir", bare, "log", "--oneline", "main"]);
fs.rmSync(root, { recursive: true, force: true });
});
test("prune:latest 指向旧快照时,删完要重指到一个还活着的", { timeout: 180000 }, () => {
const root = sandboxRoot();
const home = makeMachine(root, "m2"), bare = path.join(root, "bare.git"), work = path.join(root, "w");
git(["init", "--bare", "-b", "main", bare]);
const names = seedSnapshots(home, bare, work, 4);
const env = { ZCODE_SYNC_PASSPHRASE: PASS, ZCODE_SYNC_GIT_REMOTE: bare, ZCODE_SYNC_WORK: work };
// 手工把 latest.json 指回最老那个,再 keep=1 → 最老那个会被钉住不删
const snapRoot = path.join(work, "repo/snapshots");
const oldest = names[0];
fs.writeFileSync(path.join(work, "repo/latest.json"),
JSON.stringify({ snapshot: `snapshots/${oldest}`, host: "x", createdAt: "2020-01-01T00:00:00Z" }));
const r = run("prune.mjs", { home, args: ["--keep", "1", "--apply"], env });
assert.equal(r.code, 0, `应成功\n${r.err}`);
const left = fs.readdirSync(snapRoot);
assert.ok(left.includes(oldest), "被 latest 钉住的快照不得被删");
const latest = JSON.parse(fs.readFileSync(path.join(work, "repo/latest.json"), "utf8"));
assert.ok(left.includes(path.basename(latest.snapshot)), "latest 必须指向还存在的快照");
fs.rmSync(root, { recursive: true, force: true });
});
test("prune:--exclude 钉住指定快照;参数非法要报错", { timeout: 180000 }, () => {
const root = sandboxRoot();
const home = makeMachine(root, "m3"), bare = path.join(root, "bare.git"), work = path.join(root, "w");
git(["init", "--bare", "-b", "main", bare]);
const names = seedSnapshots(home, bare, work, 3);
const env = { ZCODE_SYNC_PASSPHRASE: PASS, ZCODE_SYNC_GIT_REMOTE: bare, ZCODE_SYNC_WORK: work };
const r = run("prune.mjs", { home, args: ["--keep", "1", "--exclude", names[0], "--apply"], env });
assert.equal(r.code, 0, `应成功\n${r.err}`);
assert.ok(fs.readdirSync(path.join(work, "repo/snapshots")).includes(names[0]), "--exclude 钉住的应保留");
// 非法参数
for (const bad of [["--keep", "0"], ["--keep", "abc"], ["--nope"]]) {
const b = run("prune.mjs", { home, args: bad, env });
assert.notEqual(b.code, 0, `应拒绝:${bad.join(" ")}`);
}
const h = run("prune.mjs", { home, args: ["--help"], env });
assert.equal(h.code, 0, "--help 应成功");
assert.match(h.out, /用法/);
fs.rmSync(root, { recursive: true, force: true });
});
test("prune:快照为空时优雅退出,不报错", { timeout: 120000 }, () => {
const root = sandboxRoot();
const home = makeMachine(root, "m4"), bare = path.join(root, "bare.git"), work = path.join(root, "w");
git(["init", "--bare", "-b", "main", bare]);
const r = run("prune.mjs", { home, env: { ZCODE_SYNC_GIT_REMOTE: bare, ZCODE_SYNC_WORK: work } });
assert.equal(r.code, 0, `应优雅退出\n${r.err}`);
assert.match(r.out, /远端尚无快照/);
fs.rmSync(root, { recursive: true, force: true });
});
/* ---------------- verify ---------------- */
test("verify:完好的快照通过校验", { timeout: 180000 }, () => {
const root = sandboxRoot();
const home = makeMachine(root, "m5"), bare = path.join(root, "bare.git"), work = path.join(root, "w");
git(["init", "--bare", "-b", "main", bare]);
seedSnapshots(home, bare, work, 1);
const env = { ZCODE_SYNC_PASSPHRASE: PASS, ZCODE_SYNC_GIT_REMOTE: bare, ZCODE_SYNC_WORK: work };
const r = run("verify.mjs", { home, env });
assert.equal(r.code, 0, `校验应通过\n${r.out}\n${r.err}`);
assert.match(r.out, /# 快照校验:/);
assert.match(r.out, /密钥束可解密/);
assert.match(r.out, /自研插件 zcode-tps@0.4.1 hash 一致/);
assert.match(r.out, /结论:快照完好|结论:快照可用/);
assert.doesNotMatch(r.out, /✗/, "不应有失败项");
fs.rmSync(root, { recursive: true, force: true });
});
test("verify:内容被篡改 → 报 hash 不符并退出 1", { timeout: 180000 }, () => {
const root = sandboxRoot();
const home = makeMachine(root, "m6"), bare = path.join(root, "bare.git"), work = path.join(root, "w");
git(["init", "--bare", "-b", "main", bare]);
seedSnapshots(home, bare, work, 1);
const env = { ZCODE_SYNC_PASSPHRASE: PASS, ZCODE_SYNC_GIT_REMOTE: bare, ZCODE_SYNC_WORK: work };
const snapRoot = path.join(work, "repo/snapshots");
const snap = path.join(snapRoot, fs.readdirSync(snapRoot)[0]);
// 篡改插件内容,manifest 里的 hash 就不对了
fs.writeFileSync(path.join(snap, "custom-plugins/zcode-tps/run.mjs"), "export const v=999; // tampered\n");
const r = run("verify.mjs", { home, env });
assert.equal(r.code, 1, "篡改必须导致退出码 1");
assert.match(r.out, /内容 hash 不符|hash 不符/);
assert.match(r.out, /结论:快照不可用/);
fs.rmSync(root, { recursive: true, force: true });
});
test("verify:口令不对 → 明确报错,且不打印密钥值", { timeout: 180000 }, () => {
const root = sandboxRoot();
const home = makeMachine(root, "m7"), bare = path.join(root, "bare.git"), work = path.join(root, "w");
git(["init", "--bare", "-b", "main", bare]);
seedSnapshots(home, bare, work, 1);
const env = { ZCODE_SYNC_PASSPHRASE: "totally-wrong-passphrase", ZCODE_SYNC_GIT_REMOTE: bare, ZCODE_SYNC_WORK: work };
const r = run("verify.mjs", { home, env });
assert.equal(r.code, 1, "解不开必须退出 1");
assert.match(r.out, /密钥束解不开/);
assert.match(r.out, /口令不对/);
// 绝不能把密文或密钥值打出来
assert.doesNotMatch(r.out, /tok-m7/, "不得打印密钥明文");
const enc = fs.readFileSync(path.join(work, "repo/snapshots", fs.readdirSync(path.join(work, "repo/snapshots"))[0], "secrets.enc"), "utf8");
assert.ok(!r.out.includes(enc.slice(20, 60)), "不得把密文片段打进输出");
fs.rmSync(root, { recursive: true, force: true });
});
test("verify --all:区分'可解密'与'口令不符',并给出可用版本", { timeout: 240000 }, () => {
const root = sandboxRoot();
const homeA = makeMachine(root, "m8a"), homeB = makeMachine(root, "m8b");
const bare = path.join(root, "bare.git"), work = path.join(root, "w");
git(["init", "--bare", "-b", "main", bare]);
// A 机用口令 1 导一个
const P1 = "passphrase-one-1111", P2 = "passphrase-two-2222";
const envA = { ZCODE_SYNC_PASSPHRASE: P1, ZCODE_SYNC_GIT_REMOTE: bare, ZCODE_SYNC_WORK: work };
assert.equal(run("export.mjs", { home: homeA, env: envA }).code, 0);
execFileSync(process.execPath, ["-e", "setTimeout(()=>{},1100)"]);
// A 机改用口令 2 再导一个(模拟换口令)
const envA2 = { ZCODE_SYNC_PASSPHRASE: P2, ZCODE_SYNC_GIT_REMOTE: bare, ZCODE_SYNC_WORK: work };
assert.equal(run("export.mjs", { home: homeA, env: envA2 }).code, 0);
// 用口令 1 扫全部:应有一个可解、一个不可解
const r = run("verify.mjs", { home: homeA, args: ["--all"], env: envA });
assert.equal(r.code, 0, `扫描应成功\n${r.err}`);
assert.match(r.out, /全部快照扫描\(2 个\)/);
assert.match(r.out, /可解密/, "应有一个可解密");
assert.match(r.out, /口令不符\/损坏/, "应有一个解不开");
assert.match(r.out, /当前口令可用:1 个/);
assert.match(r.out, /latest.json 指向的 .* 用当前口令不可用/, "应提示 latest 不可用");
assert.match(r.out, /改用可用的那版:\/sync-import /, "应给出可用版本与命令");
fs.rmSync(root, { recursive: true, force: true });
});
/* ---------------- 推送隔离 ---------------- */
test("export:只推本次快照,不把别的会话未推送的快照连带推上去", { timeout: 180000 }, () => {
const root = sandboxRoot();
const home = makeMachine(root, "m15"), bare = path.join(root, "bare.git"), work = path.join(root, "w");
git(["init", "--bare", "-b", "main", bare]);
seedSnapshots(home, bare, work, 1);
const env = { ZCODE_SYNC_PASSPHRASE: PASS, ZCODE_SYNC_GIT_REMOTE: bare, ZCODE_SYNC_WORK: work };
const repo = path.join(work, "repo");
// 冒充"另一个会话留下的、未推送的、用别的口令导出的快照"
const stray = path.join(repo, "snapshots", "stray-20200101-000000");
fs.mkdirSync(stray, { recursive: true });
fs.writeFileSync(path.join(stray, "manifest.json"), JSON.stringify({ version: 1, host: "other", createdAt: "2020-01-01T00:00:00Z" }));
fs.writeFileSync(path.join(stray, "secrets.enc"), JSON.stringify({ alg: "aes-256-gcm", kdf: "scrypt", salt: "x", iv: "y", tag: "z", data: "q" }));
// 再导一次
const r = run("export.mjs", { home, env });
assert.equal(r.code, 0, `导出应成功\n${r.err}`);
// 远端不该有这个 stray 快照
const remoteFiles = git(["--git-dir", bare, "ls-tree", "-r", "--name-only", "main"]);
assert.doesNotMatch(remoteFiles, /stray-20200101-000000/, "别的会话的未推送快照不得被连带推上去");
assert.match(remoteFiles, /latest\.json/, "本次 latest.json 应推上去");
// 本地应留下它,并在输出里说明"落下了什么"
assert.ok(fs.existsSync(stray), "本地那个快照不该被删(不是我们的东西)");
assert.match(r.out, /落下 \d+ 项非本次内容/, "应告知用户有内容被刻意落下");
assert.match(r.out, /stray-20200101-000000/, "应点名落下的是哪个");
fs.rmSync(root, { recursive: true, force: true });
});
test("export:远端 latest 指向的快照,其密钥束必须能用当前口令解开", { timeout: 180000 }, () => {
const root = sandboxRoot();
const home = makeMachine(root, "m16"), bare = path.join(root, "bare.git"), work = path.join(root, "w");
git(["init", "--bare", "-b", "main", bare]);
seedSnapshots(home, bare, work, 1);
const env = { ZCODE_SYNC_PASSPHRASE: PASS, ZCODE_SYNC_GIT_REMOTE: bare, ZCODE_SYNC_WORK: work };
const repo = path.join(work, "repo");
// 从远端克隆一份,模拟"队友拿到最新状态"
const clone = path.join(root, "teammate");
git(["clone", "-q", bare, clone]);
const latest = JSON.parse(fs.readFileSync(path.join(clone, "latest.json"), "utf8"));
const snap = path.join(clone, latest.snapshot);
assert.ok(fs.existsSync(path.join(snap, "manifest.json")), "latest 指向的快照必须存在");
assert.ok(fs.existsSync(path.join(snap, "secrets.enc")), "latest 指向的快照必须有密钥束");
// 用团队口令应能解开
assert.doesNotThrow(() => decryptSecrets(fs.readFileSync(path.join(snap, "secrets.enc"), "utf8"), PASS),
"队友用团队口令必须能解开 latest 的密钥束");
fs.rmSync(root, { recursive: true, force: true });
});
test("prune:删除也限定在 snapshots/ 范围内", { timeout: 180000 }, () => {
const root = sandboxRoot();
const home = makeMachine(root, "m17"), bare = path.join(root, "bare.git"), work = path.join(root, "w");
git(["init", "--bare", "-b", "main", bare]);
seedSnapshots(home, bare, work, 3);
const env = { ZCODE_SYNC_PASSPHRASE: PASS, ZCODE_SYNC_GIT_REMOTE: bare, ZCODE_SYNC_WORK: work };
const repo = path.join(work, "repo");
// 放一个仓库外的杂项文件,不该被 prune 的提交带上去
fs.writeFileSync(path.join(repo, "scratch-note.txt"), "someone's temp file\n");
const r = run("prune.mjs", { home, args: ["--keep", "1", "--apply"], env });
assert.equal(r.code, 0, `清理应成功\n${r.err}`);
const remoteFiles = git(["--git-dir", bare, "ls-tree", "-r", "--name-only", "main"]);
assert.doesNotMatch(remoteFiles, /scratch-note\.txt/, "无关文件不得被连带提交");
assert.ok(fs.existsSync(path.join(repo, "scratch-note.txt")), "无关文件不该被删");
fs.rmSync(root, { recursive: true, force: true });
});
/* ---------------- doctor ---------------- */
test("doctor:环境就绪时全绿", { timeout: 180000 }, () => {
const root = sandboxRoot();
const home = makeMachine(root, "m9"), bare = path.join(root, "bare.git"), work = path.join(root, "w");
git(["init", "--bare", "-b", "main", bare]);
seedSnapshots(home, bare, work, 1);
const env = { ZCODE_SYNC_PASSPHRASE: PASS, ZCODE_SYNC_GIT_REMOTE: bare, ZCODE_SYNC_WORK: work };
const r = run("doctor.mjs", { home, env });
assert.equal(r.code, 0, `应全绿\n${r.out}\n${r.err}`);
assert.match(r.out, /# zcode-env-sync 自诊断/);
assert.match(r.out, /✓ Node 版本/);
assert.match(r.out, /✓ git 可用/);
assert.match(r.out, /✓ 同步口令/);
assert.match(r.out, /✓ 换行符锁定/);
assert.match(r.out, /✓ core\.autocrlf:false/);
assert.match(r.out, /✓ latest 可解密/);
assert.match(r.out, /结论:一切就绪|结论:可正常工作/);
assert.doesNotMatch(r.out, /✗/, "不应有 fail");
fs.rmSync(root, { recursive: true, force: true });
});
test("doctor:缺口令 → fail 并给修复建议,退出码 1", { timeout: 120000 }, () => {
const root = sandboxRoot();
const home = makeMachine(root, "m10"), bare = path.join(root, "bare.git"), work = path.join(root, "w");
git(["init", "--bare", "-b", "main", bare]);
const r = run("doctor.mjs", { home, env: { ZCODE_SYNC_GIT_REMOTE: bare, ZCODE_SYNC_WORK: work } });
assert.equal(r.code, 1, "缺口令应退出 1");
assert.match(r.out, /✗ 同步口令:未设置/);
assert.match(r.out, /ZCODE_SYNC_PASSPHRASE/, "应给出修复建议");
assert.match(r.out, /结论:有 1 项|结论:有 \d+ 项必须先修/);
fs.rmSync(root, { recursive: true, force: true });
});
test("doctor:口令解不开 latest → fail 并指向 verify --all", { timeout: 180000 }, () => {
const root = sandboxRoot();
const home = makeMachine(root, "m11"), bare = path.join(root, "bare.git"), work = path.join(root, "w");
git(["init", "--bare", "-b", "main", bare]);
seedSnapshots(home, bare, work, 1);
const r = run("doctor.mjs", { home, env: {
ZCODE_SYNC_PASSPHRASE: "some-other-passphrase", ZCODE_SYNC_GIT_REMOTE: bare, ZCODE_SYNC_WORK: work } });
assert.equal(r.code, 1, "解不开应退出 1");
assert.match(r.out, /✗ latest 可解密/);
assert.match(r.out, /verify\.mjs --all/, "应指向 verify --all 去找可用版本");
fs.rmSync(root, { recursive: true, force: true });
});
test("doctor:origin 内嵌凭据 → 警告", { timeout: 180000 }, () => {
const root = sandboxRoot();
const home = makeMachine(root, "m12"), bare = path.join(root, "bare.git"), work = path.join(root, "w");
git(["init", "--bare", "-b", "main", bare]);
seedSnapshots(home, bare, work, 1);
// 手工把 token 塞进 origin,模拟历史遗留的不卫生配置
git(["-C", path.join(work, "repo"), "remote", "set-url", "origin", "https://sometoken@gitea.example.invalid/x.git"]);
const r = run("doctor.mjs", { home, env: {
ZCODE_SYNC_PASSPHRASE: PASS, ZCODE_SYNC_GIT_REMOTE: bare, ZCODE_SYNC_WORK: work } });
assert.match(r.out, /⚠ origin 地址/);
assert.match(r.out, /内嵌了凭据/);
fs.rmSync(root, { recursive: true, force: true });
});
/* ---------------- status --list ---------------- */
test("status --list:列出全部快照并标出 latest", { timeout: 180000 }, () => {
const root = sandboxRoot();
const home = makeMachine(root, "m13"), bare = path.join(root, "bare.git"), work = path.join(root, "w");
git(["init", "--bare", "-b", "main", bare]);
const names = seedSnapshots(home, bare, work, 3);
const env = { ZCODE_SYNC_PASSPHRASE: PASS, ZCODE_SYNC_GIT_REMOTE: bare, ZCODE_SYNC_WORK: work };
const r = run("status.mjs", { home, args: ["--list"], env });
assert.equal(r.code, 0, `应成功\n${r.err}`);
assert.match(r.out, /# 远端快照\(3 个/);
for (const n of names) assert.ok(r.out.includes(n), `应列出 ${n}`);
assert.match(r.out, /\* .*latest|标 \* 的是 latest/, "应标出 latest");
assert.match(r.out, /导入指定版本:\/sync-import/, "应告诉用户怎么导入指定版本");
// 无快照时优雅提示:换一个空裸仓(同一个仓 clone 下来仍有快照)
const emptyBare = path.join(root, "empty.git");
git(["init", "--bare", "-b", "main", emptyBare]);
const r2 = run("status.mjs", { home, args: ["--list"],
env: { ...env, ZCODE_SYNC_GIT_REMOTE: emptyBare, ZCODE_SYNC_WORK: path.join(root, "w2") } });
assert.equal(r2.code, 0, `应优雅退出\n${r2.err}`);
assert.match(r2.out, /远端尚无快照/);
fs.rmSync(root, { recursive: true, force: true });
});
/* ---------------- manifest 体积与 enabledDetailed ---------------- */
test("export:manifest 记录体积与 inline 插件版本", { timeout: 180000 }, () => {
const root = sandboxRoot();
const home = makeMachine(root, "m14"), bare = path.join(root, "bare.git"), work = path.join(root, "w");
git(["init", "--bare", "-b", "main", bare]);
seedSnapshots(home, bare, work, 1);
const env = { ZCODE_SYNC_PASSPHRASE: PASS, ZCODE_SYNC_GIT_REMOTE: bare, ZCODE_SYNC_WORK: work };
const latest = JSON.parse(fs.readFileSync(path.join(work, "repo/latest.json"), "utf8"));
assert.ok(latest.bytes > 0, "latest.json 应记录体积");
const m = JSON.parse(fs.readFileSync(path.join(work, "repo", latest.snapshot, "manifest.json"), "utf8"));
assert.ok(m.bytes > 0, "manifest 应记录体积");
// 体积应对得上(容一个小误差,manifest 自身写入会变大小)
assert.ok(Math.abs(m.bytes - latest.bytes) < 4096, "manifest 与 latest 的体积应一致");
assert.ok(Array.isArray(m.enabledDetailed), "应记录 enabledDetailed");
assert.ok(m.enabledDetailed.length > 0, "enabledDetailed 不应为空");
const bu = m.enabledDetailed.find((e) => e.name === "browser-use");
assert.ok(bu, "应含 browser-use");
assert.equal(bu.version, "0.4.2", "市场插件版本应从清单查到");
assert.equal(bu.source, "zcode-plugins-official");
fs.rmSync(root, { recursive: true, force: true });
});