Files
Netcatty/domain/notes.test.ts
T
2026-06-27 02:33:56 +08:00

278 lines
8.6 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import {
buildVaultNoteMarkdownExportFiles,
buildVaultNoteFromMarkdownImport,
deriveNoteImportTitle,
getVaultNotesForExportScope,
importMarkdownFilesToVaultNotes,
importMarkdownPayloadsToVaultNotes,
matchesVaultNoteSearch,
normalizeNoteGroups,
normalizeVaultNotes,
remapExpandedNoteGroupPaths,
resolveMovedNoteGroupPath,
resolveRenderedMarkdownLinkHref,
sanitizeNoteExportFileNamePart,
sanitizeNoteTitle,
sanitizeVaultNote,
} from "./notes";
test("sanitizeVaultNote supplies safe defaults", () => {
const note = sanitizeVaultNote({ title: " ", content: 123 as never });
assert.equal(note.title, "");
assert.equal(note.content, "");
assert.equal(typeof note.id, "string");
assert.equal(typeof note.createdAt, "number");
});
test("sanitizeNoteTitle preserves non-empty titles and allows empty titles", () => {
assert.equal(sanitizeNoteTitle(" My note "), "My note");
assert.equal(sanitizeNoteTitle(""), "");
assert.equal(sanitizeNoteTitle(" "), "");
assert.equal(sanitizeNoteTitle(undefined), "");
});
test("normalizeVaultNotes trims group and de-duplicates tags", () => {
const notes = normalizeVaultNotes([
{
id: "n1",
title: " Runbook ",
content: "body",
group: " Ops ",
tags: ["db", "db", " psql "],
createdAt: 1,
updatedAt: 2,
},
]);
assert.equal(notes[0].title, "Runbook");
assert.equal(notes[0].group, "Ops");
assert.deepEqual(notes[0].tags, ["db", "psql"]);
});
test("normalizeNoteGroups trims and de-duplicates groups", () => {
assert.deepEqual(normalizeNoteGroups([" Ops ", "Ops", "", 1]), ["Ops"]);
});
test("resolveMovedNoteGroupPath avoids merging with an existing target folder", () => {
assert.equal(
resolveMovedNoteGroupPath("Archive/DB", "Ops", [
"Ops",
"Ops/DB",
"Archive/DB",
"Archive/DB/Runbooks",
]),
"Ops/DB 2",
);
});
test("resolveMovedNoteGroupPath normalizes existing paths before detecting conflicts", () => {
assert.equal(
resolveMovedNoteGroupPath("Archive/DB", "Ops", [
"Ops",
"Ops / DB",
"Archive/DB",
]),
"Ops/DB 2",
);
});
test("resolveMovedNoteGroupPath rejects moving a folder into itself", () => {
assert.equal(
resolveMovedNoteGroupPath("Ops", "Ops/DB", ["Ops", "Ops/DB"]),
null,
);
});
test("remapExpandedNoteGroupPaths preserves expanded descendants after folder moves", () => {
assert.deepEqual(
[...remapExpandedNoteGroupPaths(
new Set(["Archive", "Archive/DB", "Archive/DB/Runbooks", "Ops"]),
"Archive/DB",
"Ops/DB 2",
)].sort(),
["Archive", "Ops", "Ops/DB 2", "Ops/DB 2/Runbooks"],
);
});
test("matchesVaultNoteSearch checks title, body, tags, group, and linked hosts", () => {
const note = sanitizeVaultNote({
id: "n1",
title: "Failover",
content: "Promote replica",
group: "Ops",
tags: ["postgres"],
linkedHostIds: ["h1"],
createdAt: 1,
updatedAt: 1,
});
const hosts = [{ id: "h1", label: "db-prod", hostname: "10.0.0.5" }] as never;
assert.equal(matchesVaultNoteSearch(note, "postgres", hosts), true);
assert.equal(matchesVaultNoteSearch(note, "db-prod", hosts), true);
assert.equal(matchesVaultNoteSearch(note, "missing", hosts), false);
});
test("sanitizeNoteExportFileNamePart removes unsafe path characters", () => {
assert.equal(sanitizeNoteExportFileNamePart(" a/b:c* ", "note"), "a-b-c-");
assert.equal(sanitizeNoteExportFileNamePart("a\u0001b", "note"), "a-b");
assert.equal(sanitizeNoteExportFileNamePart("..", "note"), "note");
assert.equal(sanitizeNoteExportFileNamePart("CON", "note"), "CON_");
});
test("getVaultNotesForExportScope includes only selected folder descendants", () => {
const notes = [
sanitizeVaultNote({ id: "n1", title: "Root", content: "root", createdAt: 1, updatedAt: 1, order: 1000 }),
sanitizeVaultNote({ id: "n2", title: "Ops", content: "ops", group: "Ops", createdAt: 1, updatedAt: 1, order: 2000 }),
sanitizeVaultNote({ id: "n3", title: "DB", content: "db", group: "Ops/DB", createdAt: 1, updatedAt: 1, order: 3000 }),
sanitizeVaultNote({ id: "n4", title: "Other", content: "other", group: "Other", createdAt: 1, updatedAt: 1, order: 4000 }),
];
assert.deepEqual(
getVaultNotesForExportScope(notes, { type: "group", group: "Ops" }).map((item) => item.id),
["n2", "n3"],
);
});
test("buildVaultNoteMarkdownExportFiles preserves groups and de-duplicates file names", () => {
const notes = [
sanitizeVaultNote({ id: "n1", title: "Runbook", content: "# One", group: "Ops/DB", createdAt: 1, updatedAt: 1, order: 1000 }),
sanitizeVaultNote({ id: "n2", title: "Runbook", content: "# Two", group: "Ops/DB", createdAt: 1, updatedAt: 1, order: 2000 }),
sanitizeVaultNote({ id: "n3", title: "", content: "Untitled", createdAt: 1, updatedAt: 1, order: 3000 }),
sanitizeVaultNote({ id: "n4", title: "Outside", content: "Nope", group: "Other", createdAt: 1, updatedAt: 1, order: 4000 }),
];
const files = buildVaultNoteMarkdownExportFiles(notes, { type: "group", group: "Ops" });
assert.deepEqual(files, [
{ name: "Ops/DB/Runbook.md", content: "# One" },
{ name: "Ops/DB/Runbook-2.md", content: "# Two" },
]);
assert.equal(buildVaultNoteMarkdownExportFiles(notes)[2].name, "note-3.md");
});
test("resolveRenderedMarkdownLinkHref recovers ssh links sanitized by the editor DOM", () => {
assert.equal(
resolveRenderedMarkdownLinkHref(
"Open [10.2.0.32](ssh://10.2.0.32) from notes",
"10.2.0.32",
"about:blank",
),
"ssh://10.2.0.32",
);
});
test("resolveRenderedMarkdownLinkHref keeps normal rendered links unchanged", () => {
assert.equal(
resolveRenderedMarkdownLinkHref(
"[Example](https://example.com)",
"Example",
"https://example.com/",
),
"https://example.com/",
);
});
test("resolveRenderedMarkdownLinkHref avoids guessing duplicate labels", () => {
assert.equal(
resolveRenderedMarkdownLinkHref(
"[host](ssh://10.0.0.1) [host](ssh://10.0.0.2)",
"host",
"about:blank",
),
"about:blank",
);
});
test("deriveNoteImportTitle prefers the first markdown heading", () => {
assert.equal(
deriveNoteImportTitle("runbook.md", "# Failover\n\nPromote replica"),
"Failover",
);
assert.equal(
deriveNoteImportTitle("deploy-notes.markdown", "No heading here"),
"deploy-notes",
);
assert.equal(
deriveNoteImportTitle("README.txt", ""),
"README",
);
assert.equal(
deriveNoteImportTitle("script.md", "```sh\n# not a title\n```\n\n# Real Title"),
"Real Title",
);
assert.equal(
deriveNoteImportTitle("unclosed.md", "```sh\n# not a title\n\n# Also inside fence"),
"unclosed",
);
assert.equal(
deriveNoteImportTitle("preface.md", "# Real Title\n\n```sh\n# not a title"),
"Real Title",
);
});
test("buildVaultNoteFromMarkdownImport creates a note in the target group", () => {
const note = buildVaultNoteFromMarkdownImport({
fileName: "runbook.md",
content: "# Runbook\n\nRestart sshd",
group: "Ops",
order: 1000,
});
assert.equal(note.title, "Runbook");
assert.equal(note.content, "# Runbook\n\nRestart sshd");
assert.equal(note.group, "Ops");
assert.equal(note.order, 1000);
});
test("importMarkdownPayloadsToVaultNotes appends notes from pre-read payloads", () => {
const existing = [sanitizeVaultNote({
id: "existing",
title: "Existing",
content: "body",
createdAt: 1,
updatedAt: 1,
order: 1000,
})];
const result = importMarkdownPayloadsToVaultNotes(
[{ fileName: "imported.md", content: "# Imported\n\nBody" }],
existing,
"Ops",
);
assert.equal(result.importedCount, 1);
assert.equal(result.notes.length, 2);
assert.equal(result.notes[1].title, "Imported");
assert.equal(result.notes[1].group, "Ops");
});
test("importMarkdownFilesToVaultNotes appends notes and skips unsupported files", async () => {
const existing = [sanitizeVaultNote({
id: "existing",
title: "Existing",
content: "body",
createdAt: 1,
updatedAt: 1,
order: 1000,
})];
const files = [
new File(["# Imported\n\nBody"], "imported.md", { type: "text/markdown" }),
new File(["ignored"], "notes.json", { type: "application/json" }),
];
const result = await importMarkdownFilesToVaultNotes(
files,
existing,
"Ops",
async (file) => file.text(),
);
assert.equal(result.importedCount, 1);
assert.equal(result.skippedCount, 1);
assert.equal(result.notes.length, 2);
assert.equal(result.notes[1].title, "Imported");
assert.equal(result.notes[1].group, "Ops");
});