test: recover review harness controller contract

This commit is contained in:
vorotamoroz
2026-07-20 10:14:01 +00:00
parent 0a31d30c6c
commit 71f1b63b9a
6 changed files with 1098 additions and 1 deletions
@@ -0,0 +1,289 @@
import type { ObsidianLiveSyncSettings, SettingsMigrationState } from "@vrtmrz/livesync-commonlib/settings";
import type { CompatibilityPause } from "@/common/databaseCompatibility.ts";
import type {
ReviewHarnessScenarioResult,
ReviewHarnessScenarioStatus,
} from "./reviewHarnessTypes";
export type { ReviewHarnessScenarioResult, ReviewHarnessScenarioStatus } from "./reviewHarnessTypes";
export const REVIEW_HARNESS_SCENARIOS = [
{
id: "settings-lifecycle",
title: "Settings lifecycle",
description:
"Checks whether loaded settings retain the seven synchronisation choices and apply new-Vault recommendations only to a genuinely new Vault.",
mode: "automatic",
access: "read-only",
},
{
id: "compatibility-review",
title: "Compatibility review boundary",
description:
"Checks the current device-local compatibility pause, then guides the reviewer through its explicit review and restart boundary.",
mode: "guided",
access: "device-local-state",
},
{
id: "p2p-composition",
title: "P2P composition",
description:
"Checks that the Obsidian host and P2P interface still resolve the current Commonlib replicator.",
mode: "automatic",
access: "read-only",
},
{
id: "vault-round-trip",
title: "Vault fixture round trip",
description:
"Creates, reads, modifies, renames, and removes a fixed owned fixture tree after explicit confirmation.",
mode: "automatic",
access: "dedicated-vault-fixtures",
},
] as const;
export const REVIEW_HARNESS_SCENARIO_IDS = REVIEW_HARNESS_SCENARIOS.map(({ id }) => id);
export type ReviewHarnessScenario = (typeof REVIEW_HARNESS_SCENARIOS)[number];
export type ReviewHarnessScenarioId = ReviewHarnessScenario["id"];
export type ReviewHarnessScenarioMode = ReviewHarnessScenario["mode"];
export type ReviewHarnessScenarioAccess = ReviewHarnessScenario["access"];
export const REVIEW_HARNESS_STATE_KEY = "review-harness-v1";
export interface PendingReviewRun {
readonly formatVersion: 1;
readonly requestId: string;
readonly scenarioId: "compatibility-review";
readonly stage: "awaiting-restart";
readonly requestedAt: string;
}
export type ParsedPendingReviewRun = { pendingRun?: PendingReviewRun; error?: string };
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
export function parsePendingReviewRun(serialised: string | null | undefined): ParsedPendingReviewRun {
if (!serialised) return {};
let value: unknown;
try {
value = JSON.parse(serialised) as unknown;
} catch {
return { error: "Review Harness continuation is not valid JSON" };
}
if (!isRecord(value)) return { error: "Review Harness continuation must be an object" };
if (value.formatVersion !== 1) {
return { error: `Unsupported Review Harness continuation version: ${String(value.formatVersion)}` };
}
if (value.scenarioId !== "compatibility-review") {
return { error: `Unknown Review Harness scenario: ${String(value.scenarioId)}` };
}
if (value.stage !== "awaiting-restart") {
return { error: `Unknown Review Harness continuation stage: ${String(value.stage)}` };
}
if (typeof value.requestedAt !== "string") {
return { error: "Review Harness request time must be an ISO date" };
}
const requestedAtMs = Date.parse(value.requestedAt);
if (Number.isNaN(requestedAtMs) || new Date(requestedAtMs).toISOString() !== value.requestedAt) {
return { error: "Review Harness request time must be an ISO date" };
}
if (value.requestId !== `compatibility-review-${value.requestedAt}`) {
return { error: "Review Harness request ID does not match the fixed continuation format" };
}
return {
pendingRun: {
formatVersion: 1,
requestId: value.requestId,
scenarioId: value.scenarioId,
stage: value.stage,
requestedAt: value.requestedAt,
},
};
}
const PRESERVED_SYNC_SETTING_KEYS = [
"liveSync",
"syncOnSave",
"syncOnEditorSave",
"syncOnStart",
"syncOnFileOpen",
"syncAfterMerge",
"periodicReplication",
] as const;
const NEW_VAULT_RECOMMENDATION_KEYS = [
"syncMaxSizeInMB",
"chunkSplitterVersion",
"doNotUseFixedRevisionForChunks",
"usePluginSyncV2",
"handleFilenameCaseSensitive",
"E2EEAlgorithm",
] as const;
type LifecycleSettingKey = (typeof PRESERVED_SYNC_SETTING_KEYS)[number] | (typeof NEW_VAULT_RECOMMENDATION_KEYS)[number];
type SettingsForLifecycleInspection = Partial<Pick<ObsidianLiveSyncSettings, LifecycleSettingKey>>;
export function inspectSettingsLifecycle(input: {
readonly migration: SettingsMigrationState | undefined;
readonly settings: SettingsForLifecycleInspection;
readonly newVaultSettings: SettingsForLifecycleInspection;
}): ReviewHarnessScenarioResult {
if (!input.migration) {
return {
status: "failed",
detail: "The settings service did not expose migration evidence.",
observations: [],
};
}
const invalidSyncSettings = PRESERVED_SYNC_SETTING_KEYS.filter(
(key) => typeof input.settings[key] !== "boolean"
);
if (invalidSyncSettings.length > 0) {
return {
status: "failed",
detail: `Synchronisation choices are missing or invalid: ${invalidSyncSettings.join(", ")}`,
observations: [],
};
}
const observations = PRESERVED_SYNC_SETTING_KEYS.map((key) => `${key}=${String(input.settings[key])}`);
if (!input.migration.isNewVault) {
return {
status: "passed",
detail: `Loaded an existing Vault from settings schema ${input.migration.sourceVersion} to ${input.migration.targetVersion}.`,
observations,
};
}
const mismatchedRecommendations = NEW_VAULT_RECOMMENDATION_KEYS.filter(
(key) => input.settings[key] !== input.newVaultSettings[key]
);
if (mismatchedRecommendations.length > 0) {
return {
status: "failed",
detail: `The new Vault recommendations differ for: ${mismatchedRecommendations.join(", ")}`,
observations,
};
}
return {
status: "passed",
detail: "Loaded the current new Vault recommendations without enabling a remote connection.",
observations,
};
}
export function inspectCompatibilityReview(input: {
readonly migration: SettingsMigrationState | undefined;
readonly reviewInitialised: boolean;
readonly pendingPause: CompatibilityPause | undefined;
}): ReviewHarnessScenarioResult {
if (input.migration?.requiresSyncReview && !input.reviewInitialised) {
return {
status: "failed",
detail: "The settings migration requires review, but the compatibility review was not initialised.",
observations: [],
};
}
const observations = [
`migrationReviewRequired=${String(input.migration?.requiresSyncReview ?? false)}`,
`compatibilityReviewInitialised=${String(input.reviewInitialised)}`,
`compatibilityPausePending=${String(input.pendingPause !== undefined)}`,
];
if (input.pendingPause) {
observations.push(`reasonSources=${input.pendingPause.reasons.map(({ source }) => source).join(",")}`);
observations.push(`reasonCount=${input.pendingPause.reasons.length}`);
observations.push(`resumable=${String(input.pendingPause.resumable)}`);
}
return {
status: "passed",
detail: input.pendingPause
? "A device-local compatibility review is pending."
: "No compatibility review is pending on this device.",
observations,
};
}
export interface ReviewHarnessReportScenario {
readonly id: ReviewHarnessScenarioId;
readonly title: string;
readonly mode: ReviewHarnessScenarioMode;
readonly status: ReviewHarnessScenarioStatus;
readonly detail: string;
}
export interface ReviewHarnessReportInput {
readonly generatedAt: string;
readonly environment: {
readonly pluginVersion: string;
readonly obsidianVersion: string;
readonly platform: string;
readonly userAgent: string;
readonly viewport: string;
};
readonly scenarios: readonly ReviewHarnessReportScenario[];
readonly transcript: readonly {
readonly at: string;
readonly event: string;
readonly detail?: string;
}[];
}
function tableCell(value: string): string {
return value.replace(/\|/gu, "\\|").replace(/\r?\n/gu, "<br>");
}
function table(headers: readonly string[], rows: readonly (readonly string[])[]): string {
const header = `| ${headers.map(tableCell).join(" | ")} |`;
const separator = `| ${headers.map(() => "---").join(" | ")} |`;
const body = rows.map((row) => `| ${row.map(tableCell).join(" | ")} |`);
return [header, separator, ...body].join("\n");
}
export function formatReviewHarnessReport(input: ReviewHarnessReportInput): string {
const environment = table(
["Field", "Value"],
[
["Plug-in", input.environment.pluginVersion],
["Obsidian", input.environment.obsidianVersion],
["Platform", input.environment.platform],
["User agent", input.environment.userAgent],
["Viewport", input.environment.viewport],
]
);
const scenarios = table(
["Scenario", "Mode", "Status", "Detail"],
input.scenarios.map(({ id, title, mode, status, detail }) => [
`${title} (${id})`,
mode,
status,
detail,
])
);
return `## Self-hosted LiveSync Review Harness report
Generated at \`${tableCell(input.generatedAt)}\`.
### Environment
${environment}
### Scenarios
${scenarios}
<details>
<summary>Event transcript</summary>
\`\`\`json
${JSON.stringify(input.transcript, null, 2)}
\`\`\`
</details>
This report was copied locally and was not transmitted by Self-hosted LiveSync. It intentionally omits Vault identifiers, paths, file names, file contents, remote configuration, and secrets. Review the environment information before posting because a user agent and viewport may identify the device or operating system.
`;
}
@@ -0,0 +1,216 @@
import { describe, expect, it } from "vitest";
import { NEW_VAULT_SETTINGS, type SettingsMigrationState } from "@vrtmrz/livesync-commonlib/settings";
import type { CompatibilityPause } from "@/common/databaseCompatibility.ts";
import {
REVIEW_HARNESS_SCENARIO_IDS,
formatReviewHarnessReport,
inspectCompatibilityReview,
inspectSettingsLifecycle,
parsePendingReviewRun,
} from "./reviewHarnessContract";
const preservedSyncSettings = {
liveSync: true,
syncOnSave: false,
syncOnEditorSave: true,
syncOnStart: false,
syncOnFileOpen: true,
syncAfterMerge: false,
periodicReplication: true,
};
function migration(overrides: Partial<SettingsMigrationState> = {}): SettingsMigrationState {
return {
sourceVersion: 9,
targetVersion: 10,
isNewVault: false,
isFromFutureSchema: false,
changed: true,
requiresSyncReview: true,
reviewReasons: [
{
code: "legacy-update-review-pending",
fromVersion: 9,
toVersion: 10,
},
],
...overrides,
};
}
function compatibilityPause(): CompatibilityPause {
return {
resumable: true,
reasons: [
{
source: "settings-schema",
sourceVersion: 9,
currentVersion: 10,
isFromFutureSchema: false,
resumable: true,
},
],
};
}
describe("Review Harness contract", () => {
it("accepts only a fixed one-shot restart continuation", () => {
const pendingRun = {
formatVersion: 1,
requestId: "compatibility-review-2026-07-18T12:00:00.000Z",
scenarioId: "compatibility-review",
stage: "awaiting-restart",
requestedAt: "2026-07-18T12:00:00.000Z",
};
expect(parsePendingReviewRun(JSON.stringify(pendingRun))).toEqual({ pendingRun });
expect(
parsePendingReviewRun(
JSON.stringify({ ...pendingRun, scenarioId: "arbitrary-command", command: "delete-vault" })
)
).toEqual({ error: "Unknown Review Harness scenario: arbitrary-command" });
expect(parsePendingReviewRun("not-json")).toEqual({ error: "Review Harness continuation is not valid JSON" });
expect(REVIEW_HARNESS_SCENARIO_IDS).toEqual([
"settings-lifecycle",
"compatibility-review",
"p2p-composition",
"vault-round-trip",
]);
});
it("checks that an existing Vault keeps the seven synchronisation choices typed and intact", () => {
const result = inspectSettingsLifecycle({
migration: migration(),
settings: preservedSyncSettings,
newVaultSettings: NEW_VAULT_SETTINGS,
});
expect(result.status).toBe("passed");
expect(result.detail).toContain("existing Vault");
expect(result.observations).toContain("liveSync=true");
expect(result.observations).toContain("periodicReplication=true");
});
it("checks recommended values only for a genuinely new Vault", () => {
const result = inspectSettingsLifecycle({
migration: migration({
sourceVersion: 10,
targetVersion: 10,
isNewVault: true,
changed: false,
requiresSyncReview: false,
reviewReasons: [],
}),
settings: {
...preservedSyncSettings,
syncMaxSizeInMB: NEW_VAULT_SETTINGS.syncMaxSizeInMB,
chunkSplitterVersion: NEW_VAULT_SETTINGS.chunkSplitterVersion,
doNotUseFixedRevisionForChunks: NEW_VAULT_SETTINGS.doNotUseFixedRevisionForChunks,
usePluginSyncV2: NEW_VAULT_SETTINGS.usePluginSyncV2,
handleFilenameCaseSensitive: NEW_VAULT_SETTINGS.handleFilenameCaseSensitive,
E2EEAlgorithm: NEW_VAULT_SETTINGS.E2EEAlgorithm,
},
newVaultSettings: NEW_VAULT_SETTINGS,
});
expect(result.status).toBe("passed");
expect(result.detail).toContain("new Vault recommendations");
});
it("requires the compatibility review controller to initialise when settings migration requires review", () => {
expect(
inspectSettingsLifecycle({
migration: undefined,
settings: preservedSyncSettings,
newVaultSettings: NEW_VAULT_SETTINGS,
}).status
).toBe("failed");
expect(
inspectCompatibilityReview({
migration: migration(),
reviewInitialised: false,
pendingPause: undefined,
})
).toEqual({
status: "failed",
detail: "The settings migration requires review, but the compatibility review was not initialised.",
observations: [],
});
});
it("distinguishes a completed compatibility review from an uninitialised controller", () => {
const result = inspectCompatibilityReview({
migration: migration(),
reviewInitialised: true,
pendingPause: undefined,
});
expect(result).toMatchObject({
status: "passed",
detail: "No compatibility review is pending on this device.",
});
expect(result.observations).toContain("compatibilityReviewInitialised=true");
});
it("reports only bounded compatibility evidence", () => {
const pendingPause: CompatibilityPause = {
resumable: true,
reasons: [
...compatibilityPause().reasons,
{
source: "legacy-review",
message: "/Users/reviewer/private-vault/secret-note.md",
resumable: true,
},
],
};
const result = inspectCompatibilityReview({
migration: migration(),
reviewInitialised: true,
pendingPause,
});
expect(result.status).toBe("passed");
expect(result.observations).toContain("reasonSources=settings-schema,legacy-review");
expect(result.observations).toContain("resumable=true");
expect(result.observations.join("\n")).not.toContain("private-vault");
expect(result.observations.join("\n")).not.toContain("secret-note.md");
});
it("formats a review report without accepting Vault or credential fields", () => {
const report = formatReviewHarnessReport({
generatedAt: "2026-07-18T12:30:00.000Z",
environment: {
pluginVersion: "1.0.0-beta.1",
obsidianVersion: "1.13.1",
platform: "ios",
userAgent: "Obsidian|mobile\nreview",
viewport: "390x844",
},
scenarios: [
{
id: "compatibility-review",
title: "Compatibility review boundary",
mode: "guided",
status: "passed",
detail: "Compatibility pause confirmed",
},
],
transcript: [
{
at: "2026-07-18T12:29:00.000Z",
event: "compatibility-review-completed",
},
],
});
expect(report).toContain("## Self-hosted LiveSync Review Harness report");
expect(report).toContain("Obsidian\\|mobile<br>review");
expect(report).toContain("Compatibility review boundary (compatibility-review)");
expect(report).toContain("was not transmitted");
expect(report).not.toContain("Vault name");
expect(report).not.toContain("credentials");
});
});
@@ -0,0 +1,311 @@
import type { ObsidianLiveSyncSettings, SettingsMigrationState } from "@vrtmrz/livesync-commonlib/settings";
import type { CompatibilityPause } from "@/common/databaseCompatibility.ts";
import {
REVIEW_HARNESS_SCENARIOS,
REVIEW_HARNESS_STATE_KEY,
formatReviewHarnessReport,
inspectCompatibilityReview,
inspectSettingsLifecycle,
parsePendingReviewRun,
type ReviewHarnessReportInput,
type ReviewHarnessScenarioId,
type ReviewHarnessScenarioResult,
} from "./reviewHarnessContract";
export interface ReviewHarnessRuntime {
now(): Date;
getSettings(): Partial<ObsidianLiveSyncSettings>;
getNewVaultSettings(): Partial<ObsidianLiveSyncSettings>;
getSettingsMigrationState(): SettingsMigrationState | undefined;
isCompatibilityReviewInitialised(): boolean;
getCompatibilityPause(): CompatibilityPause | undefined;
openCompatibilityReview(): Promise<void>;
getP2PComposition(): {
readonly first: unknown;
readonly second: unknown;
readonly expectedServices: unknown;
};
runVaultRoundTrip(): Promise<ReviewHarnessScenarioResult>;
readContinuation(): string | null;
writeContinuation(value: string): void;
deleteContinuation(): void;
restart(): void;
reportError(error: unknown): void;
copyText(value: string): Promise<void>;
getEnvironment(): ReviewHarnessReportInput["environment"];
}
export interface ReviewHarnessTranscriptEntry {
readonly at: string;
readonly event: string;
readonly detail?: string;
}
export interface ReviewHarnessSnapshot {
readonly results: Record<ReviewHarnessScenarioId, ReviewHarnessScenarioResult>;
readonly running: boolean;
readonly current: ReviewHarnessScenarioId | null;
readonly resumedRequestId: string | null;
readonly continuationError: string | null;
readonly transcript: readonly ReviewHarnessTranscriptEntry[];
}
const idleResult = (): ReviewHarnessScenarioResult => ({
status: "idle",
detail: "Not run",
observations: [],
});
function initialResults(): Record<ReviewHarnessScenarioId, ReviewHarnessScenarioResult> {
return Object.fromEntries(REVIEW_HARNESS_SCENARIOS.map(({ id }) => [id, idleResult()])) as Record<
ReviewHarnessScenarioId,
ReviewHarnessScenarioResult
>;
}
function inspectP2PComposition(input: ReturnType<ReviewHarnessRuntime["getP2PComposition"]>): ReviewHarnessScenarioResult {
if (input.first !== input.second) {
return {
status: "failed",
detail: "Two consecutive reads resolved different P2P replicators without a lifecycle transition.",
observations: [],
};
}
if (typeof input.first !== "object" || input.first === null) {
return {
status: "failed",
detail: "The P2P composition did not expose a current replicator.",
observations: [],
};
}
const env = "env" in input.first ? input.first.env : undefined;
const services = typeof env === "object" && env !== null && "services" in env ? env.services : undefined;
if (services !== input.expectedServices) {
return {
status: "failed",
detail: "The current P2P replicator is not bound to the active Obsidian services.",
observations: [],
};
}
return {
status: "passed",
detail: "The live P2P result resolves the current replicator and active Obsidian services.",
observations: [],
};
}
export class ReviewHarnessController {
private readonly results = initialResults();
private readonly transcript: ReviewHarnessTranscriptEntry[] = [];
private readonly listeners = new Set<() => void>();
private running = false;
private current: ReviewHarnessScenarioId | null = null;
private resumedRequestId: string | null = null;
private continuationError: string | null = null;
constructor(private readonly runtime: ReviewHarnessRuntime) {}
subscribe(listener: () => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
private notify(): void {
for (const listener of this.listeners) listener();
}
private record(event: string, detail?: string): void {
this.transcript.push({
at: this.runtime.now().toISOString(),
event,
...(detail ? { detail } : {}),
});
while (this.transcript.length > 100) this.transcript.shift();
}
snapshot(): ReviewHarnessSnapshot {
return {
results: Object.fromEntries(
REVIEW_HARNESS_SCENARIOS.map(({ id }) => [id, { ...this.results[id] }])
) as Record<ReviewHarnessScenarioId, ReviewHarnessScenarioResult>,
running: this.running,
current: this.current,
resumedRequestId: this.resumedRequestId,
continuationError: this.continuationError,
transcript: [...this.transcript],
};
}
consumeContinuation(): void {
const serialised = this.runtime.readContinuation();
if (!serialised) return;
this.runtime.deleteContinuation();
const parsed = parsePendingReviewRun(serialised);
if (!parsed.pendingRun) {
this.continuationError = parsed.error ?? "The Review Harness continuation was invalid.";
this.results["compatibility-review"] = {
status: "failed",
detail: "The stored continuation was invalid and was removed.",
observations: [],
};
this.record("continuation-rejected");
this.notify();
return;
}
this.resumedRequestId = parsed.pendingRun.requestId;
this.results["compatibility-review"] = {
status: "waiting-for-user",
detail: "Obsidian returned after the requested restart. Open the compatibility review to continue.",
observations: [`requestedAt=${parsed.pendingRun.requestedAt}`],
};
this.record("continuation-consumed", parsed.pendingRun.requestId);
this.notify();
}
async runAutomaticScenarios(): Promise<void> {
for (const id of ["settings-lifecycle", "p2p-composition"] as const) {
await this.runScenario(id);
}
}
async runAllScenarios(): Promise<void> {
for (const { id } of REVIEW_HARNESS_SCENARIOS) await this.runScenario(id);
}
private inspectCompatibilityReview(): ReviewHarnessScenarioResult {
return inspectCompatibilityReview({
migration: this.runtime.getSettingsMigrationState(),
reviewInitialised: this.runtime.isCompatibilityReviewInitialised(),
pendingPause: this.runtime.getCompatibilityPause(),
});
}
async runScenario(id: ReviewHarnessScenarioId): Promise<void> {
if (this.running) return;
this.running = true;
this.current = id;
this.results[id] = { status: "running", detail: "Running", observations: [] };
this.record("scenario-started", id);
this.notify();
try {
let result: ReviewHarnessScenarioResult;
if (id === "settings-lifecycle") {
result = inspectSettingsLifecycle({
migration: this.runtime.getSettingsMigrationState(),
settings: this.runtime.getSettings(),
newVaultSettings: this.runtime.getNewVaultSettings(),
});
} else if (id === "p2p-composition") {
result = inspectP2PComposition(this.runtime.getP2PComposition());
} else if (id === "vault-round-trip") {
result = await this.runtime.runVaultRoundTrip();
} else {
const inspection = this.inspectCompatibilityReview();
result =
inspection.status === "failed" || !this.runtime.getCompatibilityPause()
? inspection
: {
status: "waiting-for-user",
detail: "Open the device-local compatibility review and complete its explicit action.",
observations: inspection.observations,
};
}
this.results[id] = result;
this.record("scenario-updated", `${id}:${result.status}`);
} catch (error) {
this.setUnexpectedFailure(id, error);
} finally {
this.running = false;
this.current = null;
this.notify();
}
}
async openCompatibilityReview(): Promise<void> {
if (this.running) return;
this.running = true;
this.current = "compatibility-review";
this.notify();
try {
await this.runtime.openCompatibilityReview();
const inspection = this.inspectCompatibilityReview();
this.results["compatibility-review"] =
inspection.status === "passed" && !this.runtime.getCompatibilityPause()
? {
status: "passed",
detail: "The device-local compatibility pause was reviewed and cleared.",
observations: inspection.observations,
}
: inspection.status === "failed"
? inspection
: {
status: "waiting-for-user",
detail: "The device-local compatibility review remains pending.",
observations: inspection.observations,
};
this.record(
"compatibility-review-updated",
this.results["compatibility-review"].status
);
} catch (error) {
this.setUnexpectedFailure("compatibility-review", error);
} finally {
this.running = false;
this.current = null;
this.notify();
}
}
private setUnexpectedFailure(id: ReviewHarnessScenarioId, error: unknown): void {
this.runtime.reportError(error);
this.results[id] = {
status: "failed",
detail: "The scenario failed unexpectedly. Review the in-app logs for diagnostic details.",
observations: [],
};
this.record("scenario-failed", id);
}
prepareCompatibilityReviewRestart(): void {
const requestedAt = this.runtime.now().toISOString();
const pending = {
formatVersion: 1,
requestId: `compatibility-review-${requestedAt}`,
scenarioId: "compatibility-review",
stage: "awaiting-restart",
requestedAt,
} as const;
this.runtime.writeContinuation(JSON.stringify(pending));
this.results["compatibility-review"] = {
status: "waiting-for-user",
detail: "Restart requested. The one-shot continuation will be removed before the review resumes.",
observations: [],
};
this.record("restart-requested", pending.requestId);
this.notify();
this.runtime.restart();
}
createReport(): string {
return formatReviewHarnessReport({
generatedAt: this.runtime.now().toISOString(),
environment: this.runtime.getEnvironment(),
scenarios: REVIEW_HARNESS_SCENARIOS.map(({ id, title, mode }) => ({
id,
title,
mode,
status: this.results[id].status,
detail: this.results[id].detail,
})),
transcript: this.transcript,
});
}
async copyReport(): Promise<void> {
await this.runtime.copyText(this.createReport());
this.record("report-copied");
this.notify();
}
}
export { REVIEW_HARNESS_STATE_KEY };
@@ -0,0 +1,268 @@
import { describe, expect, it, vi } from "vitest";
import { NEW_VAULT_SETTINGS, type SettingsMigrationState } from "@vrtmrz/livesync-commonlib/settings";
import type { CompatibilityPause } from "@/common/databaseCompatibility.ts";
import { ReviewHarnessController, type ReviewHarnessRuntime } from "./reviewHarnessController";
function migration(): SettingsMigrationState {
return {
sourceVersion: 9,
targetVersion: 10,
isNewVault: false,
isFromFutureSchema: false,
changed: true,
requiresSyncReview: true,
reviewReasons: [
{
code: "legacy-update-review-pending",
fromVersion: 9,
toVersion: 10,
},
],
};
}
function compatibilityPause(): CompatibilityPause {
return {
resumable: true,
reasons: [
{
source: "settings-schema",
sourceVersion: 9,
currentVersion: 10,
isFromFutureSchema: false,
resumable: true,
},
],
};
}
function createRuntime(): ReviewHarnessRuntime & {
compatibilityReviewInitialised: boolean;
compatibilityPause: CompatibilityPause | undefined;
continuation: string | null;
events: string[];
} {
const services = {};
const replicator = { env: { services } };
const runtime: ReviewHarnessRuntime & {
compatibilityReviewInitialised: boolean;
compatibilityPause: CompatibilityPause | undefined;
continuation: string | null;
events: string[];
} = {
compatibilityReviewInitialised: true,
compatibilityPause: compatibilityPause(),
continuation: null,
events: [],
now: () => new Date("2026-07-18T12:00:00.000Z"),
getSettings: () => ({
liveSync: true,
syncOnSave: false,
syncOnEditorSave: true,
syncOnStart: false,
syncOnFileOpen: true,
syncAfterMerge: false,
periodicReplication: true,
}),
getNewVaultSettings: () => NEW_VAULT_SETTINGS,
getSettingsMigrationState: () => migration(),
isCompatibilityReviewInitialised() {
return this.compatibilityReviewInitialised;
},
getCompatibilityPause() {
return this.compatibilityPause;
},
openCompatibilityReview: vi.fn(async () => {
runtime.events.push("open-compatibility-review");
runtime.compatibilityPause = undefined;
}),
getP2PComposition: () => ({ first: replicator, second: replicator, expectedServices: services }),
runVaultRoundTrip: vi.fn(async () => ({
status: "passed" as const,
detail: "The owned fixture tree was exercised and removed.",
observations: [],
})),
readContinuation() {
return this.continuation;
},
writeContinuation(value) {
this.events.push("write");
this.continuation = value;
},
deleteContinuation() {
this.events.push("delete");
this.continuation = null;
},
restart: vi.fn(),
reportError: vi.fn(),
copyText: vi.fn(async () => undefined),
getEnvironment: () => ({
pluginVersion: "1.0.0-beta.1",
obsidianVersion: "1.13.1",
platform: "desktop",
userAgent: "test",
viewport: "1280x720",
}),
};
runtime.restart = vi.fn(() => runtime.events.push("restart"));
return runtime;
}
describe("ReviewHarnessController", () => {
it("runs the automatic settings and P2P composition checks", async () => {
const runtime = createRuntime();
const controller = new ReviewHarnessController(runtime);
await controller.runAutomaticScenarios();
expect(controller.snapshot().results["settings-lifecycle"].status).toBe("passed");
expect(controller.snapshot().results["p2p-composition"].status).toBe("passed");
expect(controller.snapshot().results["vault-round-trip"].status).toBe("idle");
expect(controller.snapshot().results["compatibility-review"].status).toBe("idle");
});
it("runs the Vault fixture scenario only when it is selected explicitly", async () => {
const runtime = createRuntime();
const controller = new ReviewHarnessController(runtime);
await controller.runAutomaticScenarios();
expect(runtime.runVaultRoundTrip).not.toHaveBeenCalled();
await controller.runScenario("vault-round-trip");
expect(runtime.runVaultRoundTrip).toHaveBeenCalledOnce();
expect(controller.snapshot().results["vault-round-trip"].status).toBe("passed");
});
it("does not copy unexpected runtime error details into the report", async () => {
const runtime = createRuntime();
runtime.runVaultRoundTrip = vi.fn(async () => {
throw new Error("Failed below /Users/reviewer/private-vault/secret-note.md");
});
const controller = new ReviewHarnessController(runtime);
await controller.runScenario("vault-round-trip");
const result = controller.snapshot().results["vault-round-trip"];
const report = controller.createReport();
expect(result).toMatchObject({
status: "failed",
detail: "The scenario failed unexpectedly. Review the in-app logs for diagnostic details.",
});
expect(report).not.toContain("private-vault");
expect(report).not.toContain("secret-note.md");
expect(runtime.reportError).toHaveBeenCalledOnce();
});
it("deletes a one-shot continuation before exposing the resumed guided step", () => {
const runtime = createRuntime();
runtime.continuation = JSON.stringify({
formatVersion: 1,
requestId: "compatibility-review-2026-07-18T11:59:00.000Z",
scenarioId: "compatibility-review",
stage: "awaiting-restart",
requestedAt: "2026-07-18T11:59:00.000Z",
});
const controller = new ReviewHarnessController(runtime);
controller.consumeContinuation();
expect(runtime.events).toEqual(["delete"]);
expect(controller.snapshot().results["compatibility-review"]).toMatchObject({
status: "waiting-for-user",
});
expect(controller.snapshot().resumedRequestId).toBe(
"compatibility-review-2026-07-18T11:59:00.000Z"
);
});
it("does not copy rejected continuation values into the report", () => {
const runtime = createRuntime();
runtime.continuation = JSON.stringify({
formatVersion: 1,
requestId: "review-1",
scenarioId: "/Users/reviewer/private-vault/secret-note.md",
stage: "awaiting-restart",
requestedAt: "2026-07-18T11:59:00.000Z",
});
const controller = new ReviewHarnessController(runtime);
controller.consumeContinuation();
expect(controller.snapshot().continuationError).toContain("private-vault");
expect(controller.snapshot().results["compatibility-review"].detail).toBe(
"The stored continuation was invalid and was removed."
);
expect(controller.createReport()).not.toContain("private-vault");
expect(controller.createReport()).not.toContain("secret-note.md");
});
it("does not accept an injected request ID which could enter the report transcript", () => {
const runtime = createRuntime();
runtime.continuation = JSON.stringify({
formatVersion: 1,
requestId: "/Users/reviewer/private-vault/secret-note.md",
scenarioId: "compatibility-review",
stage: "awaiting-restart",
requestedAt: "2026-07-18T11:59:00.000Z",
});
const controller = new ReviewHarnessController(runtime);
controller.consumeContinuation();
expect(controller.snapshot().results["compatibility-review"].status).toBe("failed");
expect(controller.createReport()).not.toContain("private-vault");
expect(controller.createReport()).not.toContain("secret-note.md");
});
it("persists the fixed continuation before requesting restart", () => {
const runtime = createRuntime();
const controller = new ReviewHarnessController(runtime);
controller.prepareCompatibilityReviewRestart();
expect(runtime.events).toEqual(["write", "restart"]);
expect(JSON.parse(runtime.continuation!)).toMatchObject({
formatVersion: 1,
scenarioId: "compatibility-review",
stage: "awaiting-restart",
});
});
it("passes only after the actual compatibility review clears its pause", async () => {
const runtime = createRuntime();
const controller = new ReviewHarnessController(runtime);
await controller.runScenario("compatibility-review");
await controller.openCompatibilityReview();
expect(runtime.openCompatibilityReview).toHaveBeenCalledOnce();
expect(controller.snapshot().results["compatibility-review"]).toMatchObject({
status: "passed",
detail: "The device-local compatibility pause was reviewed and cleared.",
});
});
it("remains waiting when the actual compatibility review stays paused", async () => {
const runtime = createRuntime();
runtime.openCompatibilityReview = vi.fn(async () => undefined);
const controller = new ReviewHarnessController(runtime);
await controller.runScenario("compatibility-review");
await controller.openCompatibilityReview();
expect(controller.snapshot().results["compatibility-review"].status).toBe("waiting-for-user");
});
it("copies a Markdown report after compatibility evidence is recorded", async () => {
const runtime = createRuntime();
const controller = new ReviewHarnessController(runtime);
await controller.runScenario("compatibility-review");
await controller.openCompatibilityReview();
await controller.copyReport();
expect(runtime.copyText).toHaveBeenCalledOnce();
expect(vi.mocked(runtime.copyText).mock.calls[0][0]).toContain("Compatibility review boundary");
expect(controller.snapshot().results["compatibility-review"].status).toBe("passed");
});
});
+11 -1
View File
@@ -27,6 +27,7 @@ export interface CompatibilityReviewUi {
export class CompatibilityReviewController {
private pause: CompatibilityPause | undefined;
private activeReview: Promise<void> | undefined;
private _initialised = false;
private disposed = false;
constructor(
@@ -39,6 +40,10 @@ export class CompatibilityReviewController {
return this.pause;
}
get initialised(): boolean {
return this._initialised;
}
private readAcknowledgedVersion(): string | null {
const setting = this.core.services.setting;
const currentMarker = setting.getSmallConfig(DATABASE_COMPATIBILITY_VERSION_KEY);
@@ -68,14 +73,19 @@ export class CompatibilityReviewController {
this.pause = evaluation.pause;
if (evaluation.initialiseAcknowledgedVersion) {
setting.setSmallConfig(DATABASE_COMPATIBILITY_VERSION_KEY, `${this.currentVersion}`);
this._initialised = true;
return true;
}
if (!this.pause) {
this._initialised = true;
return true;
}
if (!this.pause) return true;
if (settings.versionUpFlash === "") {
settings.versionUpFlash = COMPATIBILITY_PAUSE_SETTING_MESSAGE;
await setting.saveSettingData();
}
this._initialised = true;
return true;
}
@@ -76,8 +76,11 @@ describe("compatibility review controller", () => {
it("initialises the acknowledged version for a new Vault without showing a pause", async () => {
const fixture = createFixture({ marker: null, migration: { isNewVault: true } });
expect(fixture.controller.initialised).toBe(false);
await expect(fixture.controller.initialise()).resolves.toBe(true);
expect(fixture.controller.initialised).toBe(true);
expect(fixture.local.get(DATABASE_COMPATIBILITY_VERSION_KEY)).toBe("12");
expect(fixture.controller.pendingPause).toBeUndefined();
expect(fixture.saveSettingData).not.toHaveBeenCalled();