Improve troubleshooting and compatible setting handling

This commit is contained in:
vorotamoroz
2026-07-24 08:51:30 +00:00
parent 058da1f34c
commit d489452583
15 changed files with 868 additions and 50 deletions
+138
View File
@@ -0,0 +1,138 @@
import { access, readFile } from "node:fs/promises";
import { dirname, relative, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
type InspectionError = {
check: "current-label" | "local-reference" | "retired-label";
file: string;
detail: string;
};
export type TroubleshootingDocsInspection = {
ok: boolean;
checkedFiles: string[];
checkedLocalReferences: number;
errors: InspectionError[];
};
const guidePaths = ["docs/troubleshooting.md", "docs/recovery.md", "docs/tips/p2p-sync-tips.md"] as const;
const messageCataloguePath = "src/common/messagesJson/en.json";
const markdownLinkPattern = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^)]*["'])?\)/gu;
function repositoryRootFromThisFile(): string {
return resolve(dirname(fileURLToPath(import.meta.url)), "..");
}
function normaliseReferenceTarget(rawTarget: string): string {
const withoutAngles = rawTarget.startsWith("<") && rawTarget.endsWith(">") ? rawTarget.slice(1, -1) : rawTarget;
return decodeURIComponent(withoutAngles);
}
function isExternalReference(target: string): boolean {
return /^(?:https?:|mailto:|obsidian:)/u.test(target);
}
async function inspectLocalReferences(
repositoryRoot: string,
documentPath: string,
document: string,
errors: InspectionError[]
): Promise<number> {
let checked = 0;
for (const match of document.matchAll(markdownLinkPattern)) {
const rawTarget = match[1];
if (!rawTarget) continue;
const target = normaliseReferenceTarget(rawTarget);
if (isExternalReference(target) || target.startsWith("#")) continue;
const [pathPart] = target.split("#", 1);
if (!pathPart) continue;
checked++;
const referencedPath = resolve(repositoryRoot, dirname(documentPath), pathPart);
try {
await access(referencedPath);
} catch {
errors.push({
check: "local-reference",
file: documentPath,
detail: `Missing local reference: ${relative(repositoryRoot, referencedPath)}`,
});
}
}
return checked;
}
export async function inspectTroubleshootingDocs(
repositoryRoot = repositoryRootFromThisFile()
): Promise<TroubleshootingDocsInspection> {
const errors: InspectionError[] = [];
const documents = new Map<string, string>();
for (const guidePath of guidePaths) {
documents.set(guidePath, await readFile(resolve(repositoryRoot, guidePath), "utf8"));
}
const troubleshooting = documents.get("docs/troubleshooting.md")!;
const catalogue = JSON.parse(await readFile(resolve(repositoryRoot, messageCataloguePath), "utf8")) as Record<
string,
string
>;
const requiredMessageKeys = [
"TweakMismatchResolve.Action.UseConfigured",
"TweakMismatchResolve.Action.UseMine",
"TweakMismatchResolve.Action.UseRemote",
"TweakMismatchResolve.Action.Dismiss",
"obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown",
] as const;
for (const messageKey of requiredMessageKeys) {
const label = catalogue[messageKey];
if (!label) {
errors.push({
check: "current-label",
file: messageCataloguePath,
detail: `The English message catalogue does not define ${messageKey}.`,
});
continue;
}
if (!troubleshooting.includes(label)) {
errors.push({
check: "current-label",
file: "docs/troubleshooting.md",
detail: `The guide does not include the current UI label '${label}'.`,
});
}
}
for (const retiredLabel of ["`Update with mine`", "`Use configured`", "`Sync settings via Markdown files`"]) {
if (troubleshooting.includes(retiredLabel)) {
errors.push({
check: "retired-label",
file: "docs/troubleshooting.md",
detail: `The guide still includes the retired label ${retiredLabel}.`,
});
}
}
let checkedLocalReferences = 0;
for (const [guidePath, document] of documents) {
checkedLocalReferences += await inspectLocalReferences(repositoryRoot, guidePath, document, errors);
}
return {
ok: errors.length === 0,
checkedFiles: [...guidePaths],
checkedLocalReferences,
errors,
};
}
async function runCli(): Promise<void> {
const result = await inspectTroubleshootingDocs();
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
if (!result.ok) process.exitCode = 1;
}
const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : undefined;
if (invokedPath === import.meta.url) {
await runCli();
}
@@ -0,0 +1,17 @@
import { describe, expect, it } from "vitest";
import { inspectTroubleshootingDocs } from "./inspect-troubleshooting-docs";
describe("troubleshooting documentation contract", () => {
it("uses current English UI labels and resolves every local guide reference", async () => {
const result = await inspectTroubleshootingDocs();
expect(result.checkedFiles).toEqual([
"docs/troubleshooting.md",
"docs/recovery.md",
"docs/tips/p2p-sync-tips.md",
]);
expect(result.checkedLocalReferences).toBeGreaterThan(0);
expect(result.errors).toEqual([]);
expect(result.ok).toBe(true);
});
});