test: adopt macOS-safe real-Obsidian sessions

This commit is contained in:
vorotamoroz
2026-07-20 08:54:20 +00:00
parent 0c3c5db6c1
commit e79686d275
11 changed files with 124 additions and 12 deletions
@@ -0,0 +1,34 @@
import { VER } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { describe, expect, it, vi } from "vitest";
const { evalObsidianJson } = vi.hoisted(() => ({
evalObsidianJson: vi.fn(),
}));
vi.mock("./cli.ts", () => ({ evalObsidianJson }));
import { assertE2eCompatibilityMarker, type CompatibilityMarkerState } from "./liveSyncWorkflow.ts";
describe("compatibility marker persistence", () => {
it("waits for an accepted review to reach device-local storage", async () => {
const pending: CompatibilityMarkerState = {
vaultName: "fixture",
additionalSuffix: "-",
expectedStorageKey: "fixture--database-compatibility-version",
rawStorageValue: null,
serviceValue: "",
versionUpFlash: "",
};
const persisted: CompatibilityMarkerState = {
...pending,
rawStorageValue: `${VER}`,
serviceValue: `${VER}`,
};
evalObsidianJson.mockResolvedValueOnce(pending).mockResolvedValueOnce(persisted);
await expect(
assertE2eCompatibilityMarker("obsidian-cli", {}, { timeoutMs: 100, intervalMs: 0 })
).resolves.toEqual(persisted);
expect(evalObsidianJson).toHaveBeenCalledTimes(2);
});
});
+17 -6
View File
@@ -42,6 +42,11 @@ export type CompatibilityMarkerState = {
versionUpFlash: string;
};
export type CompatibilityMarkerWaitOptions = {
timeoutMs?: number;
intervalMs?: number;
};
export type ResumeCompatibilityReviewOptions = {
verifyMissingDeviceMarkerExplanation?: boolean;
screenshotPrefix?: string;
@@ -67,6 +72,7 @@ export type LocalDatabaseEntry = {
};
const E2E_PREFERRED_SETTINGS = {
displayLanguage: "def",
liveSync: false,
syncOnStart: false,
syncOnSave: false,
@@ -127,14 +133,19 @@ export async function readE2eCompatibilityMarker(
export async function assertE2eCompatibilityMarker(
cliBinary: string,
env: NodeJS.ProcessEnv
env: NodeJS.ProcessEnv,
options: CompatibilityMarkerWaitOptions = {}
): Promise<CompatibilityMarkerState> {
const state = await readE2eCompatibilityMarker(cliBinary, env);
if (state.serviceValue !== `${VER}`) {
throw new Error(
`The E2E compatibility marker was not available on first plug-in load: ${JSON.stringify(state)}`
);
const timeoutMs = options.timeoutMs ?? Number(process.env.E2E_OBSIDIAN_UI_TIMEOUT_MS ?? 10000);
const intervalMs = options.intervalMs ?? 100;
const deadline = Date.now() + timeoutMs;
let state = await readE2eCompatibilityMarker(cliBinary, env);
while (state.serviceValue !== `${VER}` && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, intervalMs));
state = await readE2eCompatibilityMarker(cliBinary, env);
}
if (state.serviceValue !== `${VER}`)
throw new Error(`The E2E compatibility marker was not persisted before timeout: ${JSON.stringify(state)}`);
return state;
}
@@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { hasExactCaseOnlyRename } from "./pathEntries.ts";
describe("case-only rename assertions", () => {
it("accepts only the exact new spelling", () => {
expect(hasExactCaseOnlyRename(["case-rename.md"], "Case-Rename.md", "case-rename.md")).toBe(true);
});
it("rejects the old spelling even when a case-insensitive lookup would resolve it", () => {
expect(hasExactCaseOnlyRename(["Case-Rename.md"], "Case-Rename.md", "case-rename.md")).toBe(false);
});
it("rejects an ambiguous directory containing both spellings", () => {
expect(hasExactCaseOnlyRename(["Case-Rename.md", "case-rename.md"], "Case-Rename.md", "case-rename.md")).toBe(
false
);
});
});
@@ -0,0 +1,30 @@
import { readdir } from "node:fs/promises";
import { basename, dirname, join } from "node:path";
import { hasExactCaseOnlyRename } from "./pathEntries.ts";
export async function waitForExactCaseOnlyRename(
vaultPath: string,
oldPath: string,
newPath: string,
timeoutMs = Number(process.env.E2E_OBSIDIAN_FILE_TIMEOUT_MS ?? 10000)
): Promise<void> {
const oldDirectory = dirname(oldPath);
const newDirectory = dirname(newPath);
if (oldDirectory !== newDirectory) {
throw new Error(`Case-only rename paths must share one parent directory: ${oldPath} -> ${newPath}`);
}
const oldName = basename(oldPath);
const newName = basename(newPath);
const directoryPath = join(vaultPath, newDirectory);
const deadline = Date.now() + timeoutMs;
let lastEntries: string[] = [];
while (Date.now() < deadline) {
lastEntries = await readdir(directoryPath);
if (hasExactCaseOnlyRename(lastEntries, oldName, newName)) return;
await new Promise((resolve) => setTimeout(resolve, 250));
}
throw new Error(
`Timed out waiting for exact case-only rename: ${oldPath} -> ${newPath}. Directory entries: ${JSON.stringify(lastEntries)}`
);
}
+3
View File
@@ -0,0 +1,3 @@
export function hasExactCaseOnlyRename(entries: readonly string[], oldName: string, newName: string): boolean {
return entries.includes(newName) && !entries.includes(oldName);
}