mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-18 23:32:32 +00:00
Stabilise configured Real Obsidian workflows
This commit is contained in:
@@ -10,6 +10,7 @@ vi.mock("./cli.ts", () => ({ evalObsidianJson }));
|
|||||||
import {
|
import {
|
||||||
assertE2eCompatibilityMarker,
|
assertE2eCompatibilityMarker,
|
||||||
createE2eCouchDbPluginData,
|
createE2eCouchDbPluginData,
|
||||||
|
prepareRemote,
|
||||||
waitForLiveSyncCoreReady,
|
waitForLiveSyncCoreReady,
|
||||||
type CompatibilityMarkerState,
|
type CompatibilityMarkerState,
|
||||||
} from "./liveSyncWorkflow.ts";
|
} from "./liveSyncWorkflow.ts";
|
||||||
@@ -77,3 +78,18 @@ describe("Real Obsidian core readiness", () => {
|
|||||||
expect(evalObsidianJson).toHaveBeenCalledTimes(2);
|
expect(evalObsidianJson).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("remote fixture preparation", () => {
|
||||||
|
it("waits for the remote Security Seed after resolving a new remote", async () => {
|
||||||
|
evalObsidianJson.mockReset();
|
||||||
|
evalObsidianJson.mockResolvedValueOnce({ status: "resolved", securitySeedReady: true });
|
||||||
|
|
||||||
|
await prepareRemote("obsidian-cli", {});
|
||||||
|
|
||||||
|
const evaluatedCode = String(evalObsidianJson.mock.calls[0]?.[1] ?? "");
|
||||||
|
expect(evaluatedCode.indexOf("markRemoteResolved")).toBeLessThan(
|
||||||
|
evaluatedCode.indexOf("ensurePBKDF2Salt")
|
||||||
|
);
|
||||||
|
expect(evaluatedCode).toContain("Timed out preparing the remote Security Seed");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -462,6 +462,7 @@ export function assertObsidianServiceContextContract(result: ObsidianServiceCont
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function prepareRemote(cliBinary: string, env: NodeJS.ProcessEnv): Promise<void> {
|
export async function prepareRemote(cliBinary: string, env: NodeJS.ProcessEnv): Promise<void> {
|
||||||
|
const timeoutMs = Number(process.env.E2E_OBSIDIAN_REMOTE_PREPARE_TIMEOUT_MS ?? 20000);
|
||||||
await evalObsidianJson<unknown>(
|
await evalObsidianJson<unknown>(
|
||||||
cliBinary,
|
cliBinary,
|
||||||
[
|
[
|
||||||
@@ -471,8 +472,16 @@ export async function prepareRemote(cliBinary: string, env: NodeJS.ProcessEnv):
|
|||||||
"const replicator=core.services.replicator.getActiveReplicator();",
|
"const replicator=core.services.replicator.getActiveReplicator();",
|
||||||
"await replicator.tryCreateRemoteDatabase(settings);",
|
"await replicator.tryCreateRemoteDatabase(settings);",
|
||||||
"await replicator.markRemoteResolved(settings);",
|
"await replicator.markRemoteResolved(settings);",
|
||||||
|
`const deadline=Date.now()+${JSON.stringify(timeoutMs)};`,
|
||||||
|
"let securitySeedReady=false;",
|
||||||
|
"do{",
|
||||||
|
"securitySeedReady=await replicator.ensurePBKDF2Salt(settings,false,false);",
|
||||||
|
"if(securitySeedReady) break;",
|
||||||
|
"await new Promise((resolve)=>setTimeout(resolve,250));",
|
||||||
|
"}while(Date.now()<deadline);",
|
||||||
|
"if(!securitySeedReady) throw new Error('Timed out preparing the remote Security Seed');",
|
||||||
"const status=await replicator.getRemoteStatus(settings);",
|
"const status=await replicator.getRemoteStatus(settings);",
|
||||||
"return JSON.stringify({status});",
|
"return JSON.stringify({status,securitySeedReady});",
|
||||||
"})()",
|
"})()",
|
||||||
].join(""),
|
].join(""),
|
||||||
env
|
env
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Supplies the runner-owned CouchDB fixture operations for the Security Seed
|
||||||
|
* reconnect scenario. Production code remains responsible for fetching,
|
||||||
|
* caching, and applying the Seed; this helper only validates the managed
|
||||||
|
* synchronisation-parameter document, replaces the one intended field, and
|
||||||
|
* compares document snapshots.
|
||||||
|
*
|
||||||
|
* Callers expose only short SHA-256 fingerprints. The original and replacement
|
||||||
|
* Seed values must stay inside the isolated test process and must not be
|
||||||
|
* written to diagnostics, screenshots, or machine-readable results.
|
||||||
|
*/
|
||||||
import { createHash, randomBytes } from "node:crypto";
|
import { createHash, randomBytes } from "node:crypto";
|
||||||
import type { CouchDbDocument } from "./couchdb.ts";
|
import type { CouchDbDocument } from "./couchdb.ts";
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import {
|
|||||||
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
||||||
import {
|
import {
|
||||||
assertEqual,
|
assertEqual,
|
||||||
configureCouchDb,
|
createE2eCouchDbPluginData,
|
||||||
|
createE2eObsidianDeviceLocalState,
|
||||||
prepareRemote,
|
prepareRemote,
|
||||||
pushLocalChanges,
|
pushLocalChanges,
|
||||||
waitForLiveSyncCoreReady,
|
waitForLiveSyncCoreReady,
|
||||||
@@ -311,25 +312,23 @@ async function main(): Promise<void> {
|
|||||||
cliBinary: obsidianCli.binary,
|
cliBinary: obsidianCli.binary,
|
||||||
vault,
|
vault,
|
||||||
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
|
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
|
||||||
|
pluginData: createE2eCouchDbPluginData(
|
||||||
|
{
|
||||||
|
uri: couchDb.uri,
|
||||||
|
username: couchDb.username,
|
||||||
|
password: couchDb.password,
|
||||||
|
dbName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
encrypt: true,
|
||||||
|
passphrase: e2eePassphrase,
|
||||||
|
usePathObfuscation: true,
|
||||||
|
E2EEAlgorithm: "v2",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
localStorageEntries: createE2eObsidianDeviceLocalState(vault.name),
|
||||||
});
|
});
|
||||||
await waitForLiveSyncCoreReady(obsidianCli.binary, session.cliEnv);
|
await waitForLiveSyncCoreReady(obsidianCli.binary, session.cliEnv);
|
||||||
await configureCouchDb(
|
|
||||||
obsidianCli.binary,
|
|
||||||
session.cliEnv,
|
|
||||||
{
|
|
||||||
uri: couchDb.uri,
|
|
||||||
username: couchDb.username,
|
|
||||||
password: couchDb.password,
|
|
||||||
dbName,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
encrypt: true,
|
|
||||||
passphrase: e2eePassphrase,
|
|
||||||
usePathObfuscation: true,
|
|
||||||
E2EEAlgorithm: "v2",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
await waitForLiveSyncCoreReady(obsidianCli.binary, session.cliEnv);
|
|
||||||
await prepareRemote(obsidianCli.binary, session.cliEnv);
|
await prepareRemote(obsidianCli.binary, session.cliEnv);
|
||||||
await pushLocalChanges(obsidianCli.binary, session.cliEnv);
|
await pushLocalChanges(obsidianCli.binary, session.cliEnv);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Verifies one complete Object Storage upload from a real Obsidian Vault,
|
||||||
|
* through LiveSync's local database and Journal Sync, to an S3-compatible
|
||||||
|
* service observed independently through the AWS SDK.
|
||||||
|
*
|
||||||
|
* The isolated Vault starts with Object Storage settings and the device-local
|
||||||
|
* compatibility acknowledgement already in place. Unconfigured start-up is
|
||||||
|
* intentionally inert and belongs to the onboarding scenario; compatibility
|
||||||
|
* review and visible setup have their own dedicated workflows. Supplying those
|
||||||
|
* prerequisites here keeps this scenario focused on the upload boundary.
|
||||||
|
*
|
||||||
|
* Note creation, local-database observation, one-shot synchronisation, request
|
||||||
|
* accounting, remote-object inspection, and prefix cleanup remain in one
|
||||||
|
* scenario so that a pass proves the same payload crossed every boundary.
|
||||||
|
* Separate successes would not prove that those observations belonged to the
|
||||||
|
* same upload.
|
||||||
|
*/
|
||||||
import { evalObsidianJson } from "../runner/cli.ts";
|
import { evalObsidianJson } from "../runner/cli.ts";
|
||||||
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
||||||
import {
|
import {
|
||||||
assertEqual,
|
assertEqual,
|
||||||
configureObjectStorage,
|
configureObjectStorage,
|
||||||
|
createE2eObjectStoragePluginData,
|
||||||
|
createE2eObsidianDeviceLocalState,
|
||||||
prepareRemote,
|
prepareRemote,
|
||||||
pushLocalChanges,
|
pushLocalChanges,
|
||||||
waitForLiveSyncCoreReady,
|
waitForLiveSyncCoreReady,
|
||||||
@@ -100,6 +119,11 @@ async function main(): Promise<void> {
|
|||||||
cliBinary: cli.binary,
|
cliBinary: cli.binary,
|
||||||
vault,
|
vault,
|
||||||
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
|
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
|
||||||
|
pluginData: createE2eObjectStoragePluginData({
|
||||||
|
...objectStorage,
|
||||||
|
bucketPrefix,
|
||||||
|
}),
|
||||||
|
localStorageEntries: createE2eObsidianDeviceLocalState(vault.name),
|
||||||
});
|
});
|
||||||
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
|
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { mkdir, writeFile } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
import {
|
import {
|
||||||
assertLocatorHasMinimumTouchTarget,
|
assertLocatorHasMinimumTouchTarget,
|
||||||
assertLocatorWithinSafeArea,
|
assertLocatorWithinSafeArea,
|
||||||
@@ -6,11 +8,17 @@ import {
|
|||||||
import { CURRENT_SETTING_VERSION } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const";
|
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_STATE_KEY } from "../../../src/features/ReviewHarness/reviewHarnessController.ts";
|
||||||
import { REVIEW_HARNESS_FIXTURE_ROOT } from "../../../src/features/ReviewHarness/reviewHarnessVaultFixture.ts";
|
import { REVIEW_HARNESS_FIXTURE_ROOT } from "../../../src/features/ReviewHarness/reviewHarnessVaultFixture.ts";
|
||||||
|
import { evalObsidianJson } from "../runner/cli.ts";
|
||||||
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
||||||
import { waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts";
|
import { waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts";
|
||||||
import { iPhoneSafeArea, setObsidianMobileTestMode } from "../runner/mobileUi.ts";
|
import { iPhoneSafeArea, setObsidianMobileTestMode } from "../runner/mobileUi.ts";
|
||||||
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
|
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
|
||||||
import { captureObsidianDialogue, obsidianRemoteDebuggingPort, withObsidianPage } from "../runner/ui.ts";
|
import {
|
||||||
|
captureObsidianDialogue,
|
||||||
|
captureObsidianPage,
|
||||||
|
obsidianRemoteDebuggingPort,
|
||||||
|
withObsidianPage,
|
||||||
|
} from "../runner/ui.ts";
|
||||||
import { createTemporaryVault } from "../runner/vault.ts";
|
import { createTemporaryVault } from "../runner/vault.ts";
|
||||||
|
|
||||||
const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_REVIEW_HARNESS_TIMEOUT_MS ?? 15000);
|
const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_REVIEW_HARNESS_TIMEOUT_MS ?? 15000);
|
||||||
@@ -26,6 +34,135 @@ type ReviewHarnessTestGlobal = typeof globalThis & {
|
|||||||
reviewHarnessCopiedReport?: string;
|
reviewHarnessCopiedReport?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ReviewHarnessReadinessSnapshot = {
|
||||||
|
coreAvailable: boolean;
|
||||||
|
databaseReady?: boolean;
|
||||||
|
appReady?: boolean;
|
||||||
|
configured?: boolean;
|
||||||
|
remoteType?: string;
|
||||||
|
settingVersion?: number;
|
||||||
|
suspended?: boolean;
|
||||||
|
unresolvedMessages: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const sensitiveDiagnosticLine =
|
||||||
|
/security seed|passphrase|password|credential|secret|access.?key|jwt.?key|authori[sz]ation|obsidian:\/\/setuplivesync|sls\+/iu;
|
||||||
|
const interruptedStartupMessages = [
|
||||||
|
"No replicator has been activated or has not been initialised yet.",
|
||||||
|
"Self-hosted LiveSync cannot be initialised, exiting loading.",
|
||||||
|
];
|
||||||
|
|
||||||
|
function redactDiagnosticLine(line: string): string {
|
||||||
|
if (sensitiveDiagnosticLine.test(line)) return "[REDACTED SENSITIVE LOG LINE]";
|
||||||
|
return line.replace(/\bhttps?:\/\/[^/\s:@]+:[^@\s/]+@/giu, "https://[REDACTED]@");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertNoInterruptedStartupNotice(stage: string): Promise<void> {
|
||||||
|
const notices = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
return await page.locator(".notice").allTextContents();
|
||||||
|
});
|
||||||
|
const interrupted = notices.filter((notice) =>
|
||||||
|
interruptedStartupMessages.some((message) => notice.includes(message))
|
||||||
|
);
|
||||||
|
if (interrupted.length > 0) {
|
||||||
|
throw new Error(`LiveSync emitted an interrupted-startup Notice during ${stage}: ${interrupted.join(" | ")}`);
|
||||||
|
}
|
||||||
|
console.log(`No interrupted-startup Notice observed during ${stage}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function captureReadinessFailure(
|
||||||
|
cliBinary: string,
|
||||||
|
session: ObsidianLiveSyncSession,
|
||||||
|
readinessError: unknown
|
||||||
|
): Promise<void> {
|
||||||
|
const outputDirectory = process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e";
|
||||||
|
await mkdir(outputDirectory, { recursive: true });
|
||||||
|
|
||||||
|
const captureErrors: string[] = [];
|
||||||
|
let screenshotPath: string | undefined;
|
||||||
|
try {
|
||||||
|
screenshotPath = await captureObsidianPage(
|
||||||
|
obsidianRemoteDebuggingPort(),
|
||||||
|
"review-harness-core-not-ready.png",
|
||||||
|
async () => undefined
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
captureErrors.push(`screenshot: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let readiness: ReviewHarnessReadinessSnapshot | undefined;
|
||||||
|
try {
|
||||||
|
readiness = await evalObsidianJson<ReviewHarnessReadinessSnapshot>(
|
||||||
|
cliBinary,
|
||||||
|
[
|
||||||
|
"(async()=>{",
|
||||||
|
"const core=app.plugins.plugins['obsidian-livesync']?.core;",
|
||||||
|
"if(!core)return JSON.stringify({coreAvailable:false,unresolvedMessages:[]});",
|
||||||
|
"const settings=core.services.setting.currentSettings();",
|
||||||
|
"let unresolvedMessages=[];",
|
||||||
|
"try{",
|
||||||
|
"unresolvedMessages=(await core.services.appLifecycle.getUnresolvedMessages()).flat()",
|
||||||
|
".filter((message)=>message!==undefined&&message!==null)",
|
||||||
|
".map((message)=>String(message)).slice(-50);",
|
||||||
|
"}catch(error){unresolvedMessages=[`Could not inspect unresolved messages: ${String(error)}`];}",
|
||||||
|
"return JSON.stringify({",
|
||||||
|
"coreAvailable:true,",
|
||||||
|
"databaseReady:core.services.database.isDatabaseReady(),",
|
||||||
|
"appReady:core.services.appLifecycle.isReady(),",
|
||||||
|
"configured:settings?.isConfigured===true,",
|
||||||
|
"remoteType:settings?.remoteType??'',",
|
||||||
|
"settingVersion:settings?.settingVersion,",
|
||||||
|
"suspended:core.services.appLifecycle.isSuspended(),",
|
||||||
|
"unresolvedMessages,",
|
||||||
|
"});",
|
||||||
|
"})()",
|
||||||
|
].join(""),
|
||||||
|
session.cliEnv
|
||||||
|
);
|
||||||
|
readiness.unresolvedMessages = readiness.unresolvedMessages.map(redactDiagnosticLine);
|
||||||
|
} catch (error) {
|
||||||
|
captureErrors.push(`readiness snapshot: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let recentLog: string[] = [];
|
||||||
|
try {
|
||||||
|
recentLog = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
||||||
|
const opened = await page.evaluate(
|
||||||
|
(commandId) =>
|
||||||
|
(globalThis as ReviewHarnessTestGlobal).app?.commands?.executeCommandById(commandId) === true,
|
||||||
|
"obsidian-livesync:view-log"
|
||||||
|
);
|
||||||
|
if (!opened) throw new Error("The Show log command was not registered.");
|
||||||
|
const logPane = page.locator(".logpane");
|
||||||
|
await logPane.waitFor({ state: "visible", timeout: 5000 });
|
||||||
|
return (await logPane.locator(".log pre").allTextContents()).slice(-80).map(redactDiagnosticLine);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
captureErrors.push(`recent log: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultPath = join(outputDirectory, "review-harness-core-not-ready.json");
|
||||||
|
await writeFile(
|
||||||
|
resultPath,
|
||||||
|
`${JSON.stringify(
|
||||||
|
{
|
||||||
|
capturedAt: new Date().toISOString(),
|
||||||
|
failure: readinessError instanceof Error ? readinessError.message : String(readinessError),
|
||||||
|
screenshotPath,
|
||||||
|
readiness,
|
||||||
|
recentLog,
|
||||||
|
captureErrors,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}\n`,
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
if (screenshotPath) console.error(`Review Harness core readiness screenshot: ${screenshotPath}`);
|
||||||
|
console.error(`Review Harness core readiness diagnostics: ${resultPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
async function openHarness(): Promise<void> {
|
async function openHarness(): Promise<void> {
|
||||||
const opened = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
const opened = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
||||||
return await page.evaluate(
|
return await page.evaluate(
|
||||||
@@ -203,6 +340,8 @@ async function copyAndReadReport(): Promise<string> {
|
|||||||
async function verifyMobileHarness(): Promise<string> {
|
async function verifyMobileHarness(): Promise<string> {
|
||||||
await setObsidianMobileTestMode(obsidianRemoteDebuggingPort(), true, uiTimeoutMs);
|
await setObsidianMobileTestMode(obsidianRemoteDebuggingPort(), true, uiTimeoutMs);
|
||||||
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
||||||
|
const harness = page.locator('[data-testid="review-harness"]');
|
||||||
|
if (await harness.isVisible()) return;
|
||||||
await page.evaluate(async (viewType) => {
|
await page.evaluate(async (viewType) => {
|
||||||
const plugin = (globalThis as ReviewHarnessTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
|
const plugin = (globalThis as ReviewHarnessTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
|
||||||
if (typeof plugin !== "object" || plugin === null || !("core" in plugin)) {
|
if (typeof plugin !== "object" || plugin === null || !("core" in plugin)) {
|
||||||
@@ -252,7 +391,7 @@ async function main(): Promise<void> {
|
|||||||
vault,
|
vault,
|
||||||
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
|
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
|
||||||
pluginData: {
|
pluginData: {
|
||||||
doctorProcessedVersion: "0.25.27",
|
doctorProcessedVersion: "1.0.0",
|
||||||
settingVersion: CURRENT_SETTING_VERSION,
|
settingVersion: CURRENT_SETTING_VERSION,
|
||||||
isConfigured: true,
|
isConfigured: true,
|
||||||
additionalSuffixOfDatabaseName: "",
|
additionalSuffixOfDatabaseName: "",
|
||||||
@@ -269,7 +408,20 @@ async function main(): Promise<void> {
|
|||||||
periodicReplication: true,
|
periodicReplication: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
|
await assertNoInterruptedStartupNotice("plug-in session start");
|
||||||
|
try {
|
||||||
|
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
|
||||||
|
} catch (error) {
|
||||||
|
await captureReadinessFailure(cli.binary, session, error).catch((diagnosticError: unknown) => {
|
||||||
|
console.error(
|
||||||
|
`Could not capture Review Harness readiness diagnostics: ${
|
||||||
|
diagnosticError instanceof Error ? diagnosticError.message : String(diagnosticError)
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
await assertNoInterruptedStartupNotice("core readiness");
|
||||||
await keepCompatibilityPaused();
|
await keepCompatibilityPaused();
|
||||||
await openHarness();
|
await openHarness();
|
||||||
await waitForHarness();
|
await waitForHarness();
|
||||||
|
|||||||
@@ -1,3 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Provides release evidence for the Security Seed refresh behaviour shared by
|
||||||
|
* supported platforms in real Obsidian. It verifies that an already-open
|
||||||
|
* device keeps its deliberately stale cached Seed until replication, refreshes
|
||||||
|
* from the managed CouchDB fixture before encrypting, and never restores the
|
||||||
|
* old Seed to the remote synchronisation-parameter document.
|
||||||
|
*
|
||||||
|
* The scenario uses isolated Vaults, profiles, and a random database because
|
||||||
|
* settings, the local database, the renderer process, and CouchDB must all
|
||||||
|
* participate in the result. Device A is restarted with the same Vault and
|
||||||
|
* profile, while device B is fresh. The devices run sequentially after the
|
||||||
|
* same-process stale-cache assertion because desktop Obsidian may enforce a
|
||||||
|
* single application instance; running them concurrently would test launcher
|
||||||
|
* behaviour rather than LiveSync's shared plug-in implementation.
|
||||||
|
*
|
||||||
|
* Seed replacement, A-to-B decryption, B-to-A return synchronisation, final
|
||||||
|
* remote-document comparison, error-log inspection, screenshots, and strict
|
||||||
|
* teardown remain one scenario. Together they prove that the same replacement
|
||||||
|
* Seed was used across the complete encrypted round trip and was not later
|
||||||
|
* rolled back. Independent passing checks would not establish that continuity.
|
||||||
|
* The result records fingerprints only and does not claim to cover an
|
||||||
|
* iPadOS-specific background or reconnect lifecycle.
|
||||||
|
*/
|
||||||
import { execFileSync } from "node:child_process";
|
import { execFileSync } from "node:child_process";
|
||||||
import { createHash, randomUUID } from "node:crypto";
|
import { createHash, randomUUID } from "node:crypto";
|
||||||
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||||
|
|||||||
Reference in New Issue
Block a user