feat: restore opt-in review harness

This commit is contained in:
vorotamoroz
2026-07-20 10:50:08 +00:00
parent 71f1b63b9a
commit f70779c352
14 changed files with 907 additions and 5 deletions
+1
View File
@@ -15,6 +15,7 @@ const testSteps: Step[] = [
{ name: "smoke", args: ["run", "test:e2e:obsidian:smoke"] },
{ name: "Svelte dialogue mounts", args: ["run", "test:e2e:obsidian:dialog-mounts"] },
{ name: "settings UI", args: ["run", "test:e2e:obsidian:settings-ui"] },
{ name: "Review Harness", args: ["run", "test:e2e:obsidian:review-harness"] },
{ name: "vault reflection", args: ["run", "test:e2e:obsidian:vault-reflection"] },
{ name: "CouchDB upload", args: ["run", "test:e2e:obsidian:couchdb-upload"] },
{
+317
View File
@@ -0,0 +1,317 @@
import {
assertLocatorHasMinimumTouchTarget,
assertLocatorWithinSafeArea,
assertNoHorizontalOverflow,
} from "@vrtmrz/obsidian-test-session";
import { CURRENT_SETTING_VERSION } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const";
import { REVIEW_HARNESS_STATE_KEY } from "../../../src/features/ReviewHarness/reviewHarnessController.ts";
import { REVIEW_HARNESS_FIXTURE_ROOT } from "../../../src/features/ReviewHarness/reviewHarnessVaultFixture.ts";
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
import { waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts";
import { iPhoneSafeArea, setObsidianMobileTestMode } from "../runner/mobileUi.ts";
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
import { captureObsidianDialogue, obsidianRemoteDebuggingPort, withObsidianPage } from "../runner/ui.ts";
import { createTemporaryVault } from "../runner/vault.ts";
const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_REVIEW_HARNESS_TIMEOUT_MS ?? 15000);
type ObsidianTestApp = {
commands?: { executeCommandById(commandId: string): boolean };
plugins?: { plugins: Record<string, unknown> };
vault?: { getAbstractFileByPath(path: string): unknown | null };
};
type ReviewHarnessTestGlobal = typeof globalThis & {
app?: ObsidianTestApp;
reviewHarnessCopiedReport?: string;
};
async function openHarness(): Promise<void> {
const opened = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
return await page.evaluate(
(commandId) => (globalThis as ReviewHarnessTestGlobal).app?.commands?.executeCommandById(commandId) === true,
"obsidian-livesync:open-review-harness"
);
});
if (!opened) throw new Error("The Review Harness command was not registered.");
}
async function waitForHarness(): Promise<void> {
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
await page.locator('[data-testid="review-harness"]').waitFor({ state: "visible", timeout: uiTimeoutMs });
});
}
async function keepCompatibilityPaused(): Promise<void> {
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const summary = page.locator(".modal-container").filter({
has: page.locator(".modal-title").filter({
hasText: "Synchronisation paused for compatibility review",
}),
});
await summary.waitFor({ state: "visible", timeout: uiTimeoutMs });
await summary.getByRole("button", { name: "Keep synchronisation paused" }).click({ timeout: uiTimeoutMs });
await summary.waitFor({ state: "hidden", timeout: uiTimeoutMs });
});
}
async function runAutomaticScenarios(): Promise<void> {
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const harness = page.locator('[data-testid="review-harness"]');
await harness.locator('[data-testid="review-harness-run-automatic"]').click({ timeout: uiTimeoutMs });
for (const id of ["settings-lifecycle", "p2p-composition"]) {
await harness
.locator(`[data-testid="review-harness-result-${id}"]`)
.getByText("Passed:", { exact: false })
.waitFor({ state: "visible", timeout: uiTimeoutMs });
}
});
}
async function runVaultFixture(): Promise<string> {
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
await page
.locator('[data-testid="review-harness-run-vault-round-trip"]')
.click({ timeout: uiTimeoutMs });
const confirmation = page.locator(".modal-container").filter({
has: page.getByText("Review Harness: Vault fixture access", { exact: true }),
});
await confirmation.waitFor({ state: "visible", timeout: uiTimeoutMs });
});
const screenshot = await captureObsidianDialogue(
obsidianRemoteDebuggingPort(),
"review-harness-vault-confirmation.png",
async (page) => {
const confirmation = page.locator(".modal-container").filter({
has: page.getByText("Review Harness: Vault fixture access", { exact: true }),
});
await confirmation.waitFor({ state: "visible", timeout: uiTimeoutMs });
await assertNoHorizontalOverflow(page, confirmation, { label: "Vault fixture confirmation" });
}
);
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const harness = page.locator('[data-testid="review-harness"]');
const confirmation = page.locator(".modal-container").filter({
has: page.getByText("Review Harness: Vault fixture access", { exact: true }),
});
await confirmation.getByRole("button", { name: "Yes" }).click({ timeout: uiTimeoutMs });
await harness
.locator('[data-testid="review-harness-result-vault-round-trip"]')
.getByText("Passed:", { exact: false })
.waitFor({ state: "visible", timeout: uiTimeoutMs });
const fixtureRemoved = await page.evaluate(
(root) => (globalThis as ReviewHarnessTestGlobal).app?.vault?.getAbstractFileByPath(root) === null,
REVIEW_HARNESS_FIXTURE_ROOT
);
if (!fixtureRemoved) throw new Error("The Review Harness fixture root remained after the scenario.");
});
return screenshot;
}
async function restartAndResumeHarness(): Promise<string> {
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const harness = page.locator('[data-testid="review-harness"]');
await harness
.locator('[data-testid="review-harness-run-compatibility-review"]')
.click({ timeout: uiTimeoutMs });
await harness
.locator('[data-testid="review-harness-result-compatibility-review"]')
.getByText("Waiting for review:", { exact: false })
.waitFor({ state: "visible", timeout: uiTimeoutMs });
await harness.locator('[data-testid="review-harness-restart"]').click({ timeout: uiTimeoutMs });
});
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
await page.waitForFunction(
() => {
const plugin = (globalThis as ReviewHarnessTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
if (typeof plugin !== "object" || plugin === null || !("core" in plugin)) return false;
const core = (plugin as { core: { services: { appLifecycle: { isReady(): boolean } } } }).core;
return core.services.appLifecycle.isReady();
},
undefined,
{ timeout: uiTimeoutMs * 2 }
);
});
await keepCompatibilityPaused();
await waitForHarness();
return await captureObsidianDialogue(
obsidianRemoteDebuggingPort(),
"review-harness-resumed.png",
async (page) => {
const harness = page.locator('[data-testid="review-harness"]');
await harness
.locator('[data-testid="review-harness-resumed"]')
.waitFor({ state: "visible", timeout: uiTimeoutMs });
const continuationRemoved = await page.evaluate((stateKey) => {
const plugin = (globalThis as ReviewHarnessTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
if (typeof plugin !== "object" || plugin === null || !("core" in plugin)) {
throw new Error("Self-hosted LiveSync is unavailable after restart.");
}
const core = (plugin as { core: { services: { setting: { getSmallConfig(key: string): string } } } })
.core;
return core.services.setting.getSmallConfig(stateKey) === "";
}, REVIEW_HARNESS_STATE_KEY);
if (!continuationRemoved) throw new Error("The one-shot continuation was not removed before use.");
await assertNoHorizontalOverflow(page, harness, { label: "resumed Review Harness" });
}
);
}
async function completeResumedCompatibilityStep(): Promise<void> {
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const harness = page.locator('[data-testid="review-harness"]');
await harness
.locator('[data-testid="review-harness-open-compatibility-review"]')
.click({ timeout: uiTimeoutMs });
const summary = page.locator(".modal-container").filter({
has: page.locator(".modal-title").filter({
hasText: "Synchronisation paused for compatibility review",
}),
});
await summary.waitFor({ state: "visible", timeout: uiTimeoutMs });
await summary.getByRole("button", { name: "Resume synchronisation" }).click({ timeout: uiTimeoutMs });
await summary.waitFor({ state: "hidden", timeout: uiTimeoutMs });
await harness
.locator('[data-testid="review-harness-result-compatibility-review"]')
.getByText("The device-local compatibility pause was reviewed and cleared.", { exact: false })
.waitFor({ state: "visible", timeout: uiTimeoutMs });
});
}
async function copyAndReadReport(): Promise<string> {
return await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
await page.evaluate(`
globalThis.reviewHarnessCopiedReport = undefined;
navigator.clipboard.writeText = function (value) {
globalThis.reviewHarnessCopiedReport = value;
return Promise.resolve();
};
`);
await page.locator('[data-testid="review-harness-copy-report"]').click({ timeout: uiTimeoutMs });
await page.waitForFunction(
() => typeof (globalThis as ReviewHarnessTestGlobal).reviewHarnessCopiedReport === "string",
undefined,
{ timeout: uiTimeoutMs }
);
return await page.evaluate(
() => (globalThis as ReviewHarnessTestGlobal).reviewHarnessCopiedReport ?? ""
);
});
}
async function verifyMobileHarness(): Promise<string> {
await setObsidianMobileTestMode(obsidianRemoteDebuggingPort(), true, uiTimeoutMs);
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
await page.evaluate(async (viewType) => {
const plugin = (globalThis as ReviewHarnessTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
if (typeof plugin !== "object" || plugin === null || !("core" in plugin)) {
throw new Error("Self-hosted LiveSync is unavailable in mobile test mode.");
}
const core = (plugin as {
core: { services: { API: { showWindow(type: string): Promise<void> } } };
}).core;
await core.services.API.showWindow(viewType);
}, "self-hosted-livesync-review-harness");
});
return await captureObsidianDialogue(
obsidianRemoteDebuggingPort(),
"review-harness-mobile.png",
async (page) => {
const harness = page.locator('[data-testid="review-harness"]');
await harness.waitFor({ state: "visible", timeout: uiTimeoutMs });
await assertNoHorizontalOverflow(page, harness, { label: "mobile Review Harness" });
const heading = harness.getByRole("heading", { name: "Self-hosted LiveSync review harness" });
await assertLocatorWithinSafeArea(page, heading, {
label: "mobile Review Harness heading",
safeAreaInsets: iPhoneSafeArea,
});
for (const testId of [
"review-harness-run-automatic",
"review-harness-run-full",
"review-harness-copy-report",
]) {
await assertLocatorHasMinimumTouchTarget(page, harness.locator(`[data-testid="${testId}"]`), {
label: testId,
});
}
}
);
}
async function main(): Promise<void> {
const binary = requireObsidianBinary();
const cli = discoverObsidianCli();
if (!cli.binary) throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`);
const vault = await createTemporaryVault();
let session: ObsidianLiveSyncSession | undefined;
try {
session = await startObsidianLiveSyncSession({
binary,
cliBinary: cli.binary,
vault,
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
pluginData: {
doctorProcessedVersion: "0.25.27",
settingVersion: CURRENT_SETTING_VERSION,
isConfigured: true,
additionalSuffixOfDatabaseName: "",
enableDebugTools: true,
notifyThresholdOfRemoteStorageSize: 0,
P2P_Enabled: false,
P2P_AutoStart: false,
liveSync: false,
syncOnSave: false,
syncOnEditorSave: true,
syncOnStart: false,
syncOnFileOpen: true,
syncAfterMerge: false,
periodicReplication: true,
},
});
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
await keepCompatibilityPaused();
await openHarness();
await waitForHarness();
const initialScreenshot = await captureObsidianDialogue(
obsidianRemoteDebuggingPort(),
"review-harness-initial.png",
async (page) => {
const harness = page.locator('[data-testid="review-harness"]');
await harness.waitFor({ state: "visible", timeout: uiTimeoutMs });
await assertNoHorizontalOverflow(page, harness, { label: "Review Harness" });
}
);
await runAutomaticScenarios();
const vaultConfirmationScreenshot = await runVaultFixture();
const resumedScreenshot = await restartAndResumeHarness();
await completeResumedCompatibilityStep();
const report = await copyAndReadReport();
if (!report.includes("## Self-hosted LiveSync Review Harness report")) {
throw new Error("The copied Review Harness report was not Markdown evidence.");
}
for (const forbidden of [vault.name, REVIEW_HARNESS_FIXTURE_ROOT]) {
if (report.includes(forbidden)) throw new Error(`The Review Harness report exposed local state: ${forbidden}`);
}
const mobileScreenshot = await verifyMobileHarness();
console.log(
`Review Harness passed one-shot, fixture, report, and mobile checks. Screenshots: ${[
initialScreenshot,
vaultConfirmationScreenshot,
resumedScreenshot,
mobileScreenshot,
].join(", ")}`
);
} finally {
if (session) await session.app.stop();
await vault.dispose();
}
}
main().catch((error: unknown) => {
console.error(error instanceof Error ? error.stack : error);
process.exit(1);
});