CI / config (push) Canceled after 0s
CI / test-frontend (push) Canceled after 0s
CI / test-rust (push) Canceled after 0s
CI / test-e2e (push) Canceled after 0s
CI / test-e2e-release (push) Canceled after 0s
CI / build (push) Canceled after 0s
CI / release (push) Canceled after 0s
CI / docker (push) Canceled after 0s
CI / docker-website (push) Canceled after 0s
Add Markdown-native planning, repeat rules, subtasks, reminders, transactional backup restore, Chinese account management, and the account-managed NAS migration configuration.\n\nCo-Authored-By: Codex <noreply@anthropic.com>
121 lines
3.3 KiB
TypeScript
121 lines
3.3 KiB
TypeScript
import { expect, test } from "vitest";
|
|
import {
|
|
createPlainleafBackup,
|
|
parsePlainleafBackup,
|
|
plainleafBackupFormat,
|
|
restorePlainleafBackup,
|
|
} from "./plainleaf_backup.ts";
|
|
|
|
test("creates and parses a Markdown-only backup", () => {
|
|
const backup = createPlainleafBackup(
|
|
[{ path: "任务/收件箱.md", text: "- [ ] 测试\n" }],
|
|
"2026-08-12T12:00:00.000Z",
|
|
);
|
|
expect(parsePlainleafBackup(JSON.stringify(backup))).toEqual(backup);
|
|
expect(backup.format).toBe(plainleafBackupFormat);
|
|
});
|
|
|
|
test("rejects malformed backup payloads and unsafe paths", () => {
|
|
expect(() => parsePlainleafBackup("not json")).toThrow("有效的 JSON");
|
|
expect(() => parsePlainleafBackup('{"format":"wrong","files":[]}')).toThrow(
|
|
"Plainleaf Markdown 备份包",
|
|
);
|
|
for (const path of [
|
|
"../secret.md",
|
|
"/absolute.md",
|
|
"notes.txt",
|
|
"a\\b.md",
|
|
"a//b.md",
|
|
"a/./b.md",
|
|
]) {
|
|
expect(() =>
|
|
parsePlainleafBackup(
|
|
JSON.stringify({
|
|
format: plainleafBackupFormat,
|
|
exportedAt: "",
|
|
files: [{ path, text: "x" }],
|
|
}),
|
|
),
|
|
).toThrow("无效的 Markdown 路径");
|
|
}
|
|
});
|
|
|
|
test("allows dots in names but rejects duplicate backup destinations", () => {
|
|
expect(
|
|
parsePlainleafBackup(
|
|
JSON.stringify({
|
|
format: plainleafBackupFormat,
|
|
files: [{ path: "版本/1..2.md", text: "ok" }],
|
|
}),
|
|
).files[0].path,
|
|
).toBe("版本/1..2.md");
|
|
expect(() =>
|
|
parsePlainleafBackup(
|
|
JSON.stringify({
|
|
format: plainleafBackupFormat,
|
|
files: [
|
|
{ path: "重复.md", text: "first" },
|
|
{ path: "重复.md", text: "second" },
|
|
],
|
|
}),
|
|
),
|
|
).toThrow("重复路径");
|
|
});
|
|
|
|
test("restores listed Markdown without deleting unrelated files", async () => {
|
|
const files = new Map([
|
|
["已有.md", "旧内容"],
|
|
["保留.md", "不要删除"],
|
|
]);
|
|
const backup = createPlainleafBackup([
|
|
{ path: "已有.md", text: "新内容" },
|
|
{ path: "新增.md", text: "新增内容" },
|
|
]);
|
|
await expect(
|
|
restorePlainleafBackup(backup, {
|
|
listPaths: async () => [...files.keys()],
|
|
read: async (path) => files.get(path)!,
|
|
write: async (path, text) => {
|
|
files.set(path, text);
|
|
},
|
|
remove: async (path) => {
|
|
files.delete(path);
|
|
},
|
|
}),
|
|
).resolves.toBe(2);
|
|
expect(files).toEqual(
|
|
new Map([
|
|
["已有.md", "新内容"],
|
|
["保留.md", "不要删除"],
|
|
["新增.md", "新增内容"],
|
|
]),
|
|
);
|
|
});
|
|
|
|
test("rolls back overwritten and newly created pages when restore fails", async () => {
|
|
const files = new Map([["已有.md", "旧内容"]]);
|
|
let failing = true;
|
|
const backup = createPlainleafBackup([
|
|
{ path: "已有.md", text: "新内容" },
|
|
{ path: "新增.md", text: "新增内容" },
|
|
{ path: "失败.md", text: "不会写入" },
|
|
]);
|
|
await expect(
|
|
restorePlainleafBackup(backup, {
|
|
listPaths: async () => [...files.keys()],
|
|
read: async (path) => files.get(path)!,
|
|
write: async (path, text) => {
|
|
if (path === "失败.md" && failing) {
|
|
failing = false;
|
|
throw new Error("磁盘已满");
|
|
}
|
|
files.set(path, text);
|
|
},
|
|
remove: async (path) => {
|
|
files.delete(path);
|
|
},
|
|
}),
|
|
).rejects.toThrow("磁盘已满");
|
|
expect(files).toEqual(new Map([["已有.md", "旧内容"]]));
|
|
});
|