Files
DmitryandClaude Opus 5 c676be81ec Capture current Ansible control plane state
Commit the accumulated infrastructure work that was living only in the
working tree: monitoring stack, emergency access/bot, gyro allocator,
grimmory, adguard, backup audit and the OpenCode agent definitions.

Also ignore Python bytecode, local archives and Nix/direnv artifacts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTocXkGUUazHdKKd3r9k71
2026-08-26 21:39:28 +03:00

193 lines
6.8 KiB
JavaScript

import assert from "node:assert/strict";
import { chmod, mkdir, mkdtemp, readFile, readdir, symlink, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import {
ObsidianSync,
extractUserSection,
progressPercent,
renderBookNote,
sanitizeFilename,
} from "../src/obsidian.js";
const bundle = {
book: {
id: 42,
libraryId: 1,
title: "Test Book",
readStatus: "READING",
personalRating: 4,
metadata: {
title: "Test Book",
authors: ["Ada Author"],
categories: ["Science"],
tags: ["AI"],
moods: [],
pageCount: 300,
description: "Description",
},
shelves: [{ name: "Reading" }],
},
progress: { readProgress: 0.425, koreaderProgress: { percentage: 42.5 } },
notebook: [{ type: "annotation", text: "Quote", note: "Comment" }],
annotations: [],
bookmarks: [],
notes: [],
cfiNotes: [],
reviews: [],
sessions: [],
};
test("render creates a study note with normalized progress", () => {
const note = renderBookNote(bundle, "99 System/Export/Grimmory/Covers/42.jpg");
assert.match(note, /grimmory_id: 42/);
assert.match(note, /status: active/);
assert.match(note, /grimmory_progress: 42.5/);
assert.match(note, /> Quote/);
});
test("user section survives extraction", () => {
const note = renderBookNote(bundle);
const edited = note.replace("## Связанные заметки", "## Связанные заметки\n\n- [[AI]]");
assert.match(extractUserSection(edited), /\[\[AI\]\]/);
});
test("filename and progress normalization are safe", () => {
assert.equal(sanitizeFilename('A/B: C?'), "A-B- C-");
assert.equal(progressPercent(0.5), 50);
assert.equal(progressPercent(75), 75);
});
test("syncBook creates one note in the configured vault directory", async () => {
const { vault, sync } = await tempSync();
const relative = await sync.syncBook(bundle);
const files = await readdir(path.join(vault, "Books"));
const content = await readFile(path.join(vault, relative), "utf8");
assert.equal(relative, "Books/Test Book — grimmory-42.md");
assert.deepEqual(files, ["Test Book — grimmory-42.md"]);
assert.match(content, /grimmory_id: 42/);
assert.match(content, /<!-- grimmory:user:start -->/);
});
test("syncBook preserves the user section and renames title changes without duplicates", async () => {
const { vault, sync } = await tempSync();
const first = await sync.syncBook({
...bundle,
book: { ...bundle.book, title: "Old Title", metadata: { ...bundle.book.metadata, title: "Old Title" } },
});
const edited = (await readFile(path.join(vault, first), "utf8")).replace(
"## Связанные заметки",
"## Связанные заметки\n\n- [[User Link]]",
);
await writeFile(path.join(vault, first), edited);
const second = await sync.syncBook({
...bundle,
book: { ...bundle.book, title: "New Title", metadata: { ...bundle.book.metadata, title: "New Title" } },
});
const files = await readdir(path.join(vault, "Books"));
const content = await readFile(path.join(vault, second), "utf8");
assert.equal(second, "Books/New Title — grimmory-42.md");
assert.deepEqual(files, ["New Title — grimmory-42.md"]);
assert.match(content, /\[\[User Link\]\]/);
});
test("saveCover writes only under the configured covers directory", async () => {
const { vault, sync } = await tempSync();
const client = {
cover: async () =>
new Response(Uint8Array.from([0xff, 0xd8, 0xff]), {
headers: { "content-type": "application/json" },
}),
};
const relative = await sync.saveCover(client, 42);
const bytes = await readFile(path.join(vault, relative));
assert.equal(relative, "Covers/42.jpg");
assert.deepEqual([...bytes], [0xff, 0xd8, 0xff]);
assert.throws(
() => new ObsidianSync({ vaultPath: vault, booksDir: "Books", coversDir: "../Covers", indexScript: "index.py" }),
/Path escapes Obsidian vault/,
);
});
test("syncBook ignores body IDs and refuses duplicate frontmatter IDs", async () => {
const { vault, sync } = await tempSync();
await mkdir(path.join(vault, "Books"), { recursive: true });
await writeFile(path.join(vault, "Books/unrelated.md"), "Body mentions\ngrimmory_id: 42\n");
await sync.syncBook(bundle);
assert.equal((await readdir(path.join(vault, "Books"))).length, 2);
await writeFile(
path.join(vault, "Books/duplicate.md"),
"---\ntitle: duplicate\ngrimmory_id: 42\n---\n",
);
await assert.rejects(() => sync.syncBook(bundle), /Duplicate Obsidian notes/);
});
test("syncBook refuses to overwrite a generated note with broken user markers", async () => {
const { vault, sync } = await tempSync();
const relative = await sync.syncBook(bundle);
const file = path.join(vault, relative);
const broken = (await readFile(file, "utf8")).replace("<!-- grimmory:user:end -->", "");
await writeFile(file, broken);
await assert.rejects(() => sync.syncBook(bundle), /invalid user markers/);
assert.equal(await readFile(file, "utf8"), broken);
});
test("syncBook rejects malformed or repeated frontmatter IDs", async () => {
for (const marker of ["grimmory_id: 42 # comment", "grimmory_id: 42\ngrimmory_id: 42"]) {
const { vault, sync } = await tempSync();
await mkdir(path.join(vault, "Books"), { recursive: true });
await writeFile(path.join(vault, "Books/bad.md"), `---\ntitle: Bad\n${marker}\n---\n`);
await assert.rejects(() => sync.syncBook(bundle), /Ambiguous or malformed/);
}
});
test("saveCover rejects a covers-directory symlink outside the vault", async () => {
const { vault, sync } = await tempSync();
const outside = await mkdtemp(path.join(os.tmpdir(), "grimmory-cover-outside-"));
await symlink(outside, path.join(vault, "Covers"));
let requested = false;
await assert.rejects(
() => sync.saveCover({ cover: async () => { requested = true; } }, 42),
/Resolved path escapes Obsidian vault/,
);
assert.equal(requested, false);
});
test("rebuildIndex invokes the configured script inside the temporary vault", async () => {
const { vault, sync } = await tempSync();
await writeFile(
path.join(vault, "index.py"),
[
"import os, pathlib, sys",
"pathlib.Path('index-called.txt').write_text(sys.argv[1] + '\\n' + os.getcwd())",
"print('rebuilt')",
].join("\n"),
);
await chmod(path.join(vault, "index.py"), 0o700);
assert.equal(await sync.rebuildIndex(), "rebuilt");
const marker = await readFile(path.join(vault, "index-called.txt"), "utf8");
assert.equal(marker, `rebuild\n${vault}`);
});
async function tempSync() {
const vault = await mkdtemp(path.join(os.tmpdir(), "grimmory-obsidian-test-"));
return {
vault,
sync: new ObsidianSync({
vaultPath: vault,
booksDir: "Books",
coversDir: "Covers",
indexScript: "index.py",
}),
};
}