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
+125
View File
@@ -0,0 +1,125 @@
import { NEW_VAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/settings";
import { LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger";
import type { UseP2PReplicatorResult } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult";
import type ObsidianLiveSyncPlugin from "@/main";
import type { LiveSyncCore } from "@/main";
import type { WorkspaceLeaf } from "@/deps";
import {
ReviewHarnessController,
REVIEW_HARNESS_STATE_KEY,
type ReviewHarnessRuntime,
} from "@/features/ReviewHarness/reviewHarnessController";
import { ReviewHarnessView, VIEW_TYPE_REVIEW_HARNESS } from "@/features/ReviewHarness/ReviewHarnessView";
import type { ReviewHarnessScenarioResult } from "@/features/ReviewHarness/reviewHarnessContract";
import {
REVIEW_HARNESS_FIXTURE_ROOT,
runReviewHarnessVaultRoundTrip,
} from "@/features/ReviewHarness/reviewHarnessVaultFixture";
import type { CompatibilityReviewController } from "./compatibilityReview";
async function runVaultRoundTrip(plugin: ObsidianLiveSyncPlugin): Promise<ReviewHarnessScenarioResult> {
const vault = plugin.app.vault;
return runReviewHarnessVaultRoundTrip({
confirmFixtureAccess: async () =>
(await plugin.core.services.UI.confirm.askYesNoDialog(
"This scenario creates, reads, modifies, renames, and removes one owned fixture tree. Use a dedicated test Vault. Continue?",
{
title: "Review Harness: Vault fixture access",
defaultOption: "No",
}
)) === "yes",
fixtureRootExists: () => vault.getAbstractFileByPath(REVIEW_HARNESS_FIXTURE_ROOT) !== null,
createFixtureRoot: async () => {
await vault.createFolder(REVIEW_HARNESS_FIXTURE_ROOT);
},
createFile: (path, content) => vault.create(path, content),
readFile: (file) => vault.read(file),
modifyFile: (file, content) => vault.modify(file, content),
renameFile: (file, path) => vault.rename(file, path),
filePath: (file) => file.path,
removeFixtureRoot: async () => {
const fixtureRoot = vault.getAbstractFileByPath(REVIEW_HARNESS_FIXTURE_ROOT);
if (fixtureRoot) await plugin.app.fileManager.trashFile(fixtureRoot);
},
});
}
export function useReviewHarness(
core: LiveSyncCore,
plugin: ObsidianLiveSyncPlugin,
p2p: UseP2PReplicatorResult,
compatibilityReview: CompatibilityReviewController
): ReviewHarnessController {
const services = core.services;
const runtime: ReviewHarnessRuntime = {
now: () => new Date(),
getSettings: () => services.setting.currentSettings(),
getNewVaultSettings: () => NEW_VAULT_SETTINGS,
getSettingsMigrationState: () => services.setting.getSettingsMigrationState(),
isCompatibilityReviewInitialised: () => compatibilityReview.initialised,
getCompatibilityPause: () => compatibilityReview.pendingPause,
openCompatibilityReview: () => compatibilityReview.openReview(),
getP2PComposition: () => ({
first: p2p.replicator,
second: p2p.replicator,
expectedServices: services,
}),
runVaultRoundTrip: () => runVaultRoundTrip(plugin),
readContinuation: () => services.setting.getSmallConfig(REVIEW_HARNESS_STATE_KEY),
writeContinuation: (value) => services.setting.setSmallConfig(REVIEW_HARNESS_STATE_KEY, value),
deleteContinuation: () => services.setting.deleteSmallConfig(REVIEW_HARNESS_STATE_KEY),
restart: () => services.appLifecycle.performRestart(),
reportError: (error) => services.API.addLog(error, LOG_LEVEL_NOTICE),
copyText: async (value) => {
if (!activeWindow.navigator.clipboard) throw new Error("Clipboard access is unavailable on this device.");
await activeWindow.navigator.clipboard.writeText(value);
},
getEnvironment: () => ({
pluginVersion: services.API.getPluginVersion(),
obsidianVersion: services.API.getAppVersion(),
platform: services.API.getPlatform(),
userAgent: activeWindow.navigator.userAgent || "unavailable",
viewport:
typeof activeWindow.innerWidth === "number" && typeof activeWindow.innerHeight === "number"
? `${activeWindow.innerWidth}x${activeWindow.innerHeight}`
: "unavailable",
}),
};
const controller = new ReviewHarnessController(runtime);
let continuationConsumed = false;
let registered = false;
let openAfterLayout = false;
services.appLifecycle.onSettingLoaded.addHandler(() => {
if (!continuationConsumed) {
continuationConsumed = true;
controller.consumeContinuation();
}
const snapshot = controller.snapshot();
openAfterLayout = snapshot.resumedRequestId !== null || snapshot.continuationError !== null;
if (!services.setting.currentSettings().enableDebugTools || registered) return Promise.resolve(true);
registered = true;
services.API.registerWindow(
VIEW_TYPE_REVIEW_HARNESS,
(leaf: WorkspaceLeaf) => new ReviewHarnessView(leaf, controller)
);
services.API.addCommand({
id: "open-review-harness",
name: "Open review harness",
callback: () => {
void services.API.showWindow(VIEW_TYPE_REVIEW_HARNESS);
},
});
return Promise.resolve(true);
});
services.appLifecycle.onLayoutReady.addHandler(() => {
if (openAfterLayout && services.setting.currentSettings().enableDebugTools) {
void services.API.showWindow(VIEW_TYPE_REVIEW_HARNESS);
}
return Promise.resolve(true);
});
return controller;
}
@@ -0,0 +1,158 @@
import { describe, expect, it, vi } from "vitest";
import type { CompatibilityPause } from "@/common/databaseCompatibility.ts";
import { REVIEW_HARNESS_STATE_KEY } from "@/features/ReviewHarness/reviewHarnessController.ts";
import { useReviewHarness } from "./useReviewHarness.ts";
const VIEW_TYPE_REVIEW_HARNESS = "self-hosted-livesync-review-harness";
vi.mock("@/features/ReviewHarness/ReviewHarnessView", () => ({
VIEW_TYPE_REVIEW_HARNESS: "self-hosted-livesync-review-harness",
ReviewHarnessView: class {},
}));
function compatibilityPause(): CompatibilityPause {
return {
resumable: true,
reasons: [
{
source: "settings-schema",
sourceVersion: 9,
currentVersion: 10,
isFromFutureSchema: false,
resumable: true,
},
],
};
}
function createFixture(options: { enableDebugTools?: boolean; continuation?: string } = {}) {
const settingLoadedHandlers: Array<() => Promise<boolean>> = [];
const layoutReadyHandlers: Array<() => Promise<boolean>> = [];
const local = new Map<string, string>();
if (options.continuation) local.set(REVIEW_HARNESS_STATE_KEY, options.continuation);
const settings = {
enableDebugTools: options.enableDebugTools ?? true,
liveSync: false,
syncOnSave: false,
syncOnEditorSave: true,
syncOnStart: false,
syncOnFileOpen: true,
syncAfterMerge: false,
periodicReplication: true,
};
const services = {} as Record<string, unknown>;
const replicator = { env: { services } };
const api = {
registerWindow: vi.fn(),
addCommand: vi.fn(),
showWindow: vi.fn().mockResolvedValue(undefined),
addLog: vi.fn(),
getPluginVersion: () => "1.0.0-rc.0",
getAppVersion: () => "1.12.7",
getPlatform: () => "desktop",
};
Object.assign(services, {
setting: {
currentSettings: () => settings,
getSettingsMigrationState: () => ({
sourceVersion: 9,
targetVersion: 10,
isNewVault: false,
isFromFutureSchema: false,
changed: true,
requiresSyncReview: true,
reviewReasons: [],
}),
getSmallConfig: (key: string) => local.get(key) ?? "",
setSmallConfig: (key: string, value: string) => local.set(key, value),
deleteSmallConfig: (key: string) => local.delete(key),
},
appLifecycle: {
onSettingLoaded: {
addHandler: vi.fn((handler: () => Promise<boolean>) => settingLoadedHandlers.push(handler)),
},
onLayoutReady: {
addHandler: vi.fn((handler: () => Promise<boolean>) => layoutReadyHandlers.push(handler)),
},
performRestart: vi.fn(),
},
API: api,
UI: {
confirm: { askYesNoDialog: vi.fn().mockResolvedValue("no") },
},
});
let pause: CompatibilityPause | undefined = compatibilityPause();
const compatibilityReview = {
initialised: true,
get pendingPause() {
return pause;
},
openReview: vi.fn(async () => {
pause = undefined;
}),
};
const core = { services };
const plugin = {
core,
app: {
vault: {},
fileManager: {},
},
};
const controller = useReviewHarness(core as never, plugin as never, { replicator } as never, compatibilityReview as never);
return {
controller,
api,
settings,
local,
settingLoadedHandlers,
layoutReadyHandlers,
compatibilityReview,
};
}
describe("Review Harness composition", () => {
it("does not register the command or view when developer debug tools are disabled", async () => {
const fixture = createFixture({ enableDebugTools: false });
await fixture.settingLoadedHandlers[0]();
expect(fixture.api.registerWindow).not.toHaveBeenCalled();
expect(fixture.api.addCommand).not.toHaveBeenCalled();
});
it("removes a one-shot continuation before reopening the Harness after layout", async () => {
const requestedAt = "2026-07-18T11:59:00.000Z";
const continuation = JSON.stringify({
formatVersion: 1,
requestId: `compatibility-review-${requestedAt}`,
scenarioId: "compatibility-review",
stage: "awaiting-restart",
requestedAt,
});
const fixture = createFixture({ continuation });
await fixture.settingLoadedHandlers[0]();
expect(fixture.local.has(REVIEW_HARNESS_STATE_KEY)).toBe(false);
expect(fixture.controller.snapshot().resumedRequestId).toBe(`compatibility-review-${requestedAt}`);
expect(fixture.api.registerWindow).toHaveBeenCalledWith(VIEW_TYPE_REVIEW_HARNESS, expect.any(Function));
await fixture.layoutReadyHandlers[0]();
expect(fixture.api.showWindow).toHaveBeenCalledWith(VIEW_TYPE_REVIEW_HARNESS);
});
it("uses the actual compatibility controller as the guided review boundary", async () => {
const fixture = createFixture();
await fixture.controller.runScenario("compatibility-review");
expect(fixture.controller.snapshot().results["compatibility-review"].status).toBe("waiting-for-user");
await fixture.controller.openCompatibilityReview();
expect(fixture.compatibilityReview.openReview).toHaveBeenCalledOnce();
expect(fixture.controller.snapshot().results["compatibility-review"].status).toBe("passed");
});
});