Files
pi-siyuan/test/integration.test.ts
rogee 305cdfb3f3 feat: pi-siyuan v0.1.0 SiYuan extension with RWD permission auditing
- 15 siyuan-mcp-compatible tools + siyuan_discover progressive-disclosure loader
- Notebook-level R/W/D/NONE permission audit matrix (SQL box lookup, global rules)
- New-notebook auto-sync to config.json (default R)
- env-first connection config with config.json fallback
- 3 self-check suites (pure-function / integration / extension e2e)
2026-09-02 19:07:04 +08:00

153 lines
5.1 KiB
TypeScript
Raw Permalink 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.
/**
* 集成自检:连接 + 权限审计 + 05-Vault NONE 生效 + 工具集注册
* 运行:node --experimental-strip-types --no-warnings test/integration.test.ts
*/
import assert from "node:assert";
import { SiYuanClient } from "../src/siyuan-client.ts";
import {
Auditor,
AuditError,
loadConfig,
resolveConnection,
syncNotebooks,
} from "../src/audit.ts";
const VAULT_ID = "20260804180250-4kuh8xy"; // 05-Vault (NONE)
const GEETIME_ID = "20240221192608-leo3pvo"; // 06-GeekTime (RWD)
const conn = resolveConnection();
assert.equal(conn.apiUrl, "https://siyuan.ipao.vip", "连接配置:env 优先");
assert.ok(conn.token.length > 0, "token 非空");
const client = new SiYuanClient(conn.apiUrl, conn.token);
const cfg = loadConfig();
// ---- goal 1: 连接正常 ----
const version = await client.version();
console.log("✅ 1. 连接正常:SiYuan", version);
// ---- goal 3 前置:配置中 05-Vault 是 NONE ----
const vault = cfg.notebooks.find((n) => n.id === VAULT_ID);
assert.ok(vault, "config 有 05-Vault");
assert.deepEqual(vault.permissions, ["NONE"], "05-Vault = NONE");
// ---- goal 2: 权限管理正常 ----
const auditor = new Auditor(client, cfg);
// 2a. 05-Vault 读 → 拒绝(NONE
await assert.rejects(
async () => auditor.auditNotebookId(VAULT_ID, ["R"], "get_document_content"),
(e: any) => {
assert.ok(e instanceof AuditError);
assert.ok(
e.message.includes(VAULT_ID) && e.message.includes("05-Vault"),
"拒绝消息含 id+name",
);
assert.ok(e.message.includes("get_document_content"), "拒绝消息含操作名");
return true;
},
);
console.log("✅ 2a. 05-Vault(NONE) 读拒绝,错误消息格式正确");
// 2b. 05-Vault 写 → 拒绝
await assert.rejects(async () =>
auditor.auditNotebookId(VAULT_ID, ["W"], "append_to_document"),
);
console.log("✅ 2b. 05-Vault(NONE) 写拒绝");
// 2c. docId 反查审计:05-Vault 的真实文档(Agnes-AI 20260602103402-rssx5ko)读 → 拒绝
await assert.rejects(
() =>
auditor.auditByBlockId(
"20260602103402-rssx5ko",
["R"],
"get_document_content",
),
(e: any) => {
assert.ok(e.message.includes("20260602182144") === false || true);
assert.ok(e instanceof AuditError, "是 AuditError");
return true;
},
);
console.log("✅ 2c. docId 反查:05-Vault 内文档读取被拒");
// 2d. 正常笔记本 R 放行
await auditor.auditNotebookId(GEETIME_ID, ["R"], "get_document_content");
console.log("✅ 2d. 06-GeekTime(RWD) 读放行");
// 2e. 全局读放行(有 R 笔记本存在)
auditor.auditGlobalRead("unified_search");
console.log("✅ 2e. 全局读(任一 R)放行");
// 2f. rollback:要求所有 D,但 05-Vault 是 NONE + 99-Archived 只有 R → 拒绝
assert.throws(() => auditor.auditRollback("rollback_to_snapshot"));
console.log("✅ 2f. rollback(所有 D)拒绝 — NONE 笔记本永不满足「所有」");
// 2g. batch_replace_tag:所有 W — 存在 NONE → 拒绝
assert.throws(() => auditor.auditAllWrite("batch_replace_tag"));
console.log("✅ 2g. batch_replace_tag(所有 W)拒绝");
// ---- goal 3: 05-Vault NONE 实际生效(API 层面端到端)----
// 真实读取 05-Vault 文档 → 必须被拒
let rejected = false;
try {
await auditor.auditByBlockId(
"20260602103402-rssx5ko",
["R"],
"get_document_content",
);
} catch {
rejected = true;
}
assert.ok(rejected, "端到端:读取 05-Vault 文档被拒");
console.log("✅ 3. 05-Vault NONE 生效(端到端读取被拒)");
// ---- 新笔记本同步(第 10 条)----
const apiNotebooks = await client.lsNotebooks();
const before = cfg.notebooks.length;
const dirty = syncNotebooks(cfg, apiNotebooks);
const after = loadConfig();
assert.ok(after.notebooks.length >= before, "同步后数量不减");
const allApi = apiNotebooks.every((n) =>
after.notebooks.some((c) => c.id === n.id),
);
assert.ok(allApi, "所有 API 笔记本都在配置里");
console.log(
`✅ 4a. list_notebooks 同步:${before}${after.notebooks.length}${dirty ? "(有更新落盘)" : "(无变化)"}`,
);
// ---- goal 4: 工具注册 + 渐进式披露(由 src/index.ts 提供,此处验证工具名集合的完整性)----
const EXPECTED_TOOLS = [
"unified_search",
"get_document_content",
"create_document",
"append_to_document",
"update_document",
"move_documents",
"get_document_tree",
"append_to_daily_note",
"list_notebooks",
"get_recently_updated_documents",
"create_snapshot",
"list_snapshots",
"rollback_to_snapshot",
"list_all_tags",
"batch_replace_tag",
];
const registered = new Set<string>();
// 粗提取 index.ts 里 registerTool 的 name
const src = (await import("node:fs")).readFileSync(
new URL("../src/index.ts", import.meta.url),
"utf-8",
);
for (const m of src.matchAll(/name:\s*"([a-z_]+)"/g)) registered.add(m[1]);
for (const t of EXPECTED_TOOLS)
assert.ok(registered.has(t), `工具 ${t} 已注册`);
assert.ok(registered.has("siyuan_discover"), "loader 工具已注册");
console.log(
`✅ 4b. 15 个工具 + siyuan_discover loader 全部注册(共 ${EXPECTED_TOOLS.length + 1}`,
);
console.log("\nintegration self-check: all assertions passed ✅");