- 补回 node:path 的 dirname 导入:healShadowPath 引用它,缺导入时 ReferenceError 被自身 catch 吞掉,影子路径自愈静默失效 - 新增 pushMcpConfig:swap+flag 之后、reload 之前把活配置 mcp 推进 运行中 OC(SDK config.update → body 形参 → experimental 三连), 推送成功+reload 双保险,失败降级原单通道 - toast 区分双保险/单通道文案;log/toast 版本标识升级 v9 - 单测新增 S0 回归断言:源文件 path 导入必须含 dirname (prelude 复制的导入曾掩盖源码缺失,15/15 假绿 -> 16/16)
106 lines
4.6 KiB
JavaScript
106 lines
4.6 KiB
JavaScript
// readMcpCfg 单测:从 oc-session-all.js 源文件动态提取解析区块来测,
|
||
// 保证测的是源码本体(不是复制的逻辑)。
|
||
// 场景:1) 键值格式 2) 旧三行格式+自动迁移 3) 路径失效自愈
|
||
import { readFileSync, writeFileSync, mkdtempSync, existsSync, rmSync, mkdirSync } from "node:fs"
|
||
import { tmpdir } from "node:os"
|
||
import { join, dirname, basename } from "node:path"
|
||
|
||
const SRC = join(import.meta.dirname, "oc-session-all.js")
|
||
const text = readFileSync(SRC, "utf8")
|
||
|
||
// 抽取 CFG_TXT 定义到 activeHasMcp 之前的全部函数(含 ON_FLAG/writeMcpCfg/heal/readMcpCfg)
|
||
const start = text.indexOf("const CFG_TXT =")
|
||
const end = text.indexOf("function activeHasMcp")
|
||
if (start < 0 || end < 0 || end < start) {
|
||
console.error("EXTRACT_FAIL: markers not found")
|
||
process.exit(1)
|
||
}
|
||
const block = text.slice(start, end)
|
||
|
||
const work = mkdtempSync(join(tmpdir(), "mcpcfg-test-"))
|
||
const LOG_DIR = work
|
||
const CFG_TXT = join(work, "mcpcfg.txt")
|
||
const ON_FLAG = join(work, "mcp.on.flag")
|
||
function log(m) {
|
||
/* silent in tests */
|
||
}
|
||
|
||
const moduleSrc = `
|
||
import { existsSync, readFileSync, writeFileSync, readdirSync } from "node:fs"
|
||
import { join, basename, dirname } from "node:path"
|
||
const LOG_DIR = ${JSON.stringify(LOG_DIR)}
|
||
function log(m) {}
|
||
${block}
|
||
export { readMcpCfg, parseMode, writeMcpCfg }
|
||
`
|
||
const modPath = join(work, "extracted.mjs")
|
||
writeFileSync(modPath, moduleSrc, "utf8")
|
||
const { pathToFileURL } = await import("node:url")
|
||
const { readMcpCfg } = await import(pathToFileURL(modPath).href)
|
||
|
||
let pass = 0
|
||
let fail = 0
|
||
function check(name, cond, extra) {
|
||
if (cond) {
|
||
pass++
|
||
console.log("PASS " + name)
|
||
} else {
|
||
fail++
|
||
console.log("FAIL " + name + (extra ? " | " + extra : ""))
|
||
}
|
||
}
|
||
|
||
// ---- 回归:源文件 path 导入必须含 dirname ----
|
||
// healShadowPath 用了 dirname;源文件缺导入时 ReferenceError 会被它自己的
|
||
// catch 吞掉 → 影子自愈静默失效。prelude 里复制的导入会掩盖这个缺失
|
||
// (曾导致 15/15 假绿),所以必须对源文件本体断言。
|
||
const pathImportLine = (text.match(/^import\s*\{[^}]*\}\s*from\s*["']path["']/m) || [""])[0]
|
||
check(
|
||
"S0 源文件 path 导入含 dirname",
|
||
/dirname/.test(pathImportLine),
|
||
"import line=" + pathImportLine,
|
||
)
|
||
|
||
// ---- 场景1:键值格式(注释+中文冒号)----
|
||
const active = join(work, "opencode.json")
|
||
const shadow = join(work, "opencode.json.2")
|
||
writeFileSync(active, JSON.stringify({ mcp: { servers: { a: {} } }, tag: "on" }))
|
||
writeFileSync(shadow, JSON.stringify({ tag: "quiet" }))
|
||
writeFileSync(
|
||
CFG_TXT,
|
||
"# 注释行\n模式: 自动\n含MCP的配置: " + active + "\n安静配置: " + shadow + "\n",
|
||
"utf8",
|
||
)
|
||
let r = readMcpCfg()
|
||
check("S1 mode=auto", r.mode === "auto", "got=" + r.mode)
|
||
check("S1 active识别", r.activePath === active, "got=" + r.activePath)
|
||
check("S1 无error", !r.error, "err=" + r.error)
|
||
check("S1 不迁移(已是键值)", !r.migrated, "migrated=" + r.migrated)
|
||
|
||
// ---- 场景2:旧三行格式 -> 解析 + 自动迁移写回 ----
|
||
writeFileSync(CFG_TXT, "手动\n" + active + "\n" + shadow + "\n", "utf8")
|
||
r = readMcpCfg()
|
||
check("S2 mode=manual", r.mode === "manual", "got=" + r.mode)
|
||
check("S2 active识别", r.activePath === active, "got=" + r.activePath)
|
||
check("S2 迁移写回", existsSync(CFG_TXT) && readFileSync(CFG_TXT, "utf8").includes("模式:"), "no kv rewrite")
|
||
check("S2 迁移后路径保留", readFileSync(CFG_TXT, "utf8").includes(shadow), "shadow lost")
|
||
check("S2 再读仍正确", readMcpCfg().mode === "manual" && !readMcpCfg().error)
|
||
|
||
// ---- 场景3:真实 bug 复现 —— 含MCP行指向的备份文件被删(.1 不存在)----
|
||
const gone = join(work, "opencode.json.1")
|
||
writeFileSync(CFG_TXT, "模式: 自动\n含MCP的配置: " + gone + "\n安静配置: " + active + "\n", "utf8")
|
||
r = readMcpCfg()
|
||
check("S3 自愈后无error", !r.error, "err=" + r.error)
|
||
check("S3 healed=true", !!r.healed, "healed=" + r.healed)
|
||
check("S3 active=活文件", r.activePath === active, "ap=" + r.activePath)
|
||
check("S3 影子=存在的互补文件", r.shadowPath === shadow || r.quietPath === shadow || r.onPath === shadow,
|
||
"shadow=" + r.shadowPath + " on=" + r.onPath + " quiet=" + r.quietPath)
|
||
const rewritten = readFileSync(CFG_TXT, "utf8")
|
||
check("S3 写回含存在的影子路径", rewritten.includes(shadow), "rewritten=" + rewritten.replace(/\n/g, "|"))
|
||
check("S3 写回后二次读取仍OK", (() => { const r2 = readMcpCfg(); return !r2.error && r2.activePath === active })(),
|
||
"reread=" + JSON.stringify({ e: readMcpCfg().error }))
|
||
|
||
rmSync(work, { recursive: true, force: true })
|
||
console.log(`\nRESULT: ${pass} passed, ${fail} failed`)
|
||
process.exit(fail ? 1 : 0)
|