test: cover Adaptive WebDAV in real Obsidian

This commit is contained in:
vorotamoroz
2026-08-02 07:57:16 +00:00
parent 1fb32cb625
commit d6fd2fa4c8
9 changed files with 828 additions and 10 deletions
@@ -0,0 +1,576 @@
/**
* Exercises Adaptive Journal WebDAV through two sequential real Obsidian
* devices. The first device uses visible manual onboarding and the endpoint
* safety check. The second device imports only the Setup URI generated by the
* first device. Text and binary payloads then make one return journey.
*
* Commonlib adapter tests own the HTTP failure matrix, and the CLI E2E owns
* external Packs and both retrieval policies. This scenario remains focused
* on the Obsidian composition, persisted profile, and onboarding boundaries.
*/
import { spawn } from "node:child_process";
import { randomBytes } from "node:crypto";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { parseWebDAVConnectionURI } from "@vrtmrz/livesync-commonlib/journal-storage";
import { evalObsidianJson } from "../runner/cli.ts";
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
import {
assertEqual,
pushLocalChanges,
waitForLiveSyncCoreReady,
waitForLocalDatabaseEntry,
} from "../runner/liveSyncWorkflow.ts";
import { REMOTE_ACTIVITY_EXPECTED_STATE, waitForRemoteActivityState } from "../runner/remoteActivity.ts";
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
import {
acknowledgeDisabledOptionalFeatures,
captureAndStartInitialisation,
captureGuideDialogue,
confirmFastFetch,
confirmRebuild,
enterSetupURI,
finishInitialisation,
generateSetupURIFromDevice,
modalByTitle,
readSetupState,
resumeCompatibilityReviewIfShown,
selectRadioOption,
skipMissingRemoteConfiguration,
type SetupArtifact,
type SetupCaptureNames,
type SetupState,
} from "../runner/setupUri.ts";
import { captureObsidianPage, withObsidianPage } from "../runner/ui.ts";
import { createTemporaryVault, type TemporaryVault } from "../runner/vault.ts";
import {
assertWebDAVReachable,
deleteWebDAVPrefix,
listWebDAVObjectKeys,
loadWebDAVConfig,
makeUniqueWebDAVPrefix,
readWebDAVObjectText,
type WebDAVConfig,
} from "../runner/webDAV.ts";
process.env.E2E_OBSIDIAN_CLI_TIMEOUT_MS ??= "90000";
process.env.E2E_OBSIDIAN_SETUP_INITIALISATION_TIMEOUT_MS ??= "180000";
const supportedArguments = new Set(["--keep-webdav", "--manage-webdav"]);
const unsupportedArguments = process.argv.slice(2).filter((argument) => !supportedArguments.has(argument));
if (unsupportedArguments.length > 0) {
throw new Error(`Unsupported Adaptive WebDAV argument: ${unsupportedArguments.join(", ")}`);
}
const manageWebDAV = process.argv.includes("--manage-webdav");
const keepWebDAVFixture = process.argv.includes("--keep-webdav");
const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_SETUP_URI_TIMEOUT_MS ?? 30000);
const remoteTimeoutMs = Number(process.env.E2E_OBSIDIAN_WEBDAV_TIMEOUT_MS ?? 30000);
const captures: SetupCaptureNames = { scenario: "adaptive-webdav", guide: "adaptive-webdav" };
const secondDeviceCaptures: SetupCaptureNames = {
scenario: "adaptive-webdav-second-device",
guide: "adaptive-webdav-second-device",
};
const textPath = "E2E/adaptive-webdav/round-trip.md";
const binaryPath = "E2E/adaptive-webdav/round-trip.bin";
const firstText = "# Adaptive WebDAV\n\nCreated by the first real Obsidian device.\n";
const secondText = "# Adaptive WebDAV\n\nUpdated by the second real Obsidian device.\n";
const binaryLength = 256 * 1024;
const firstBinarySeed = 0x1a2b3c4d;
const secondBinarySeed = 0x5e6f7788;
type RunnerContext = {
binary: string;
cliBinary: string;
activeSessions: Set<ObsidianLiveSyncSession>;
};
function npmBinary(): string {
return process.platform === "win32" ? "npm.cmd" : "npm";
}
function runNpmScript(script: string): Promise<void> {
return new Promise((resolve, reject) => {
const child = spawn(npmBinary(), ["run", script], {
cwd: process.cwd(),
env: process.env,
stdio: "inherit",
});
child.on("error", reject);
child.on("exit", (code, signal) => {
if (code === 0) {
resolve();
return;
}
reject(new Error(`${script} failed with ${signal ? `signal ${signal}` : `exit code ${code}`}.`));
});
});
}
async function dismissRedundantExternalOpenPrompt(port: number): Promise<void> {
await withObsidianPage(port, async (page) => {
const prompt = modalByTitle(page, "Run action from external link?");
if (!(await prompt.isVisible({ timeout: 2000 }).catch(() => false))) return;
await prompt.getByRole("button", { name: "Cancel", exact: true }).click({ timeout: uiTimeoutMs });
await prompt.waitFor({ state: "hidden", timeout: uiTimeoutMs });
});
}
async function startSession(context: RunnerContext, vault: TemporaryVault): Promise<ObsidianLiveSyncSession> {
const session = await startObsidianLiveSyncSession({
binary: context.binary,
cliBinary: context.cliBinary,
vault,
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
});
context.activeSessions.add(session);
// Obsidian 1.12 can ask whether to repeat the CLI's 'open' action when the
// isolated profile has already restored this exact Vault. The session has
// already verified the active Vault, so dismiss only that redundant host
// prompt before exercising LiveSync UI.
await dismissRedundantExternalOpenPrompt(session.remoteDebuggingPort);
return session;
}
async function stopSession(context: RunnerContext, session: ObsidianLiveSyncSession): Promise<void> {
if (!context.activeSessions.has(session)) return;
await session.app.stop();
context.activeSessions.delete(session);
}
async function stopSessions(context: RunnerContext): Promise<void> {
for (const session of [...context.activeSessions]) await stopSession(context, session);
}
async function captureFailure(session: ObsidianLiveSyncSession, label: string): Promise<void> {
const screenshot = await captureObsidianPage(
session.remoteDebuggingPort,
`adaptive-webdav-${label}-failure.png`,
async () => undefined
).catch(() => undefined);
if (screenshot) console.error(`Adaptive WebDAV failure screenshot: ${screenshot}`);
}
async function enterManualAdaptiveWebDAVSettings(
port: number,
webDAV: WebDAVConfig,
prefix: string,
vaultPassphrase: string
): Promise<string[]> {
const screenshots: string[] = [];
await withObsidianPage(port, async (page) => {
const invitation = page.locator(".notice").filter({ hasText: "Welcome to Self-hosted LiveSync" });
await invitation.waitFor({ state: "visible", timeout: uiTimeoutMs });
await invitation.locator(".sls-onboarding-invitation-action").click({ timeout: uiTimeoutMs });
const intro = modalByTitle(page, "Welcome to Self-hosted LiveSync");
await intro.waitFor({ state: "visible", timeout: uiTimeoutMs });
await selectRadioOption(intro, "I am setting this up for the first time");
await intro
.getByRole("button", { name: "Yes, I want to set up a new synchronisation" })
.click({ timeout: uiTimeoutMs });
const method = modalByTitle(page, "Connection Method");
await method.waitFor({ state: "visible", timeout: uiTimeoutMs });
await selectRadioOption(method, "Configure a remote manually");
await method.getByRole("button", { name: "Proceed with manual configuration" }).click({ timeout: uiTimeoutMs });
const encryption = modalByTitle(page, "End-to-End Encryption");
await encryption.waitFor({ state: "visible", timeout: uiTimeoutMs });
await encryption
.locator("label.row")
.filter({ hasText: "End-to-End Encryption" })
.locator('input[type="checkbox"]')
.first()
.check({ timeout: uiTimeoutMs });
await encryption.locator('input[name="e2ee-passphrase"]').fill(vaultPassphrase);
});
screenshots.push(await captureGuideDialogue(port, "guide-adaptive-webdav-encryption.png", "End-to-End Encryption"));
await withObsidianPage(port, async (page) => {
await modalByTitle(page, "End-to-End Encryption")
.getByRole("button", { name: "Proceed", exact: true })
.click({ timeout: uiTimeoutMs });
const remoteSelection = modalByTitle(page, "Choose a synchronisation remote");
await remoteSelection.waitFor({ state: "visible", timeout: uiTimeoutMs });
await selectRadioOption(remoteSelection, "WebDAV Journal");
await remoteSelection
.getByRole("button", { name: "Continue to WebDAV setup", exact: true })
.click({ timeout: uiTimeoutMs });
const webDAVModal = modalByTitle(page, "WebDAV Journal Configuration");
await webDAVModal.waitFor({ state: "visible", timeout: uiTimeoutMs });
await webDAVModal.locator('input[name="webdav-endpoint"]').fill(webDAV.endpoint);
await webDAVModal.locator('input[name="webdav-username"]').fill(webDAV.username);
await webDAVModal.locator('input[name="webdav-password"]').fill(webDAV.password);
await webDAVModal.locator('input[name="webdav-prefix"]').fill(prefix);
await webDAVModal.locator('input[name="webdav-use-internal-api"]').check({ timeout: uiTimeoutMs });
await webDAVModal.locator("summary").filter({ hasText: "Advanced Settings" }).click();
await webDAVModal.locator('select[name="webdav-journal-format"]').selectOption("adaptive-v1");
const packPolicy = webDAVModal.locator('select[name="webdav-pack-read-policy"]');
await packPolicy.waitFor({ state: "visible", timeout: uiTimeoutMs });
assertEqual(
await packPolicy.inputValue(),
"whole-pack",
"Adaptive WebDAV did not present complete Pack retrieval as the default."
);
if (
(await webDAVModal
.getByRole("button", { name: "Continue with verified settings", exact: true })
.count()) !== 0
) {
throw new Error("Adaptive WebDAV could continue before its endpoint safety check completed.");
}
await webDAVModal
.getByRole("button", { name: "Run endpoint safety check", exact: true })
.click({ timeout: uiTimeoutMs });
await webDAVModal
.getByText("Required Adaptive operations are supported by this WebDAV endpoint.", { exact: false })
.waitFor({ state: "visible", timeout: uiTimeoutMs });
await webDAVModal
.getByText("Exact HTTP byte-range retrieval is supported.", { exact: false })
.waitFor({ state: "visible", timeout: uiTimeoutMs });
await webDAVModal
.getByRole("button", { name: "Continue with verified settings", exact: true })
.waitFor({ state: "visible", timeout: uiTimeoutMs });
if ((await webDAVModal.getByRole("button", { name: "Save without connecting", exact: true }).count()) !== 0) {
throw new Error("Adaptive WebDAV onboarding exposed the unverified Settings-only save action.");
}
});
screenshots.push(
await captureGuideDialogue(port, "guide-adaptive-webdav-safety-check.png", "WebDAV Journal Configuration")
);
await withObsidianPage(port, async (page) => {
const webDAVModal = modalByTitle(page, "WebDAV Journal Configuration");
await webDAVModal
.getByRole("button", { name: "Continue with verified settings", exact: true })
.click({ timeout: uiTimeoutMs });
await modalByTitle(page, "Setup Complete: Preparing to Initialise Server").waitFor({
state: "visible",
timeout: uiTimeoutMs,
});
});
return screenshots;
}
function assertAdaptiveWebDAVSettings(state: SetupState, webDAV: WebDAVConfig, prefix: string, label: string): void {
assertEqual(state.remoteType, "WEBDAV", `${label} did not retain WebDAV as its active remote.`);
assertEqual(state.journalFormat, "adaptive-v1", `${label} did not retain the Adaptive Journal format.`);
assertEqual(state.packReadPolicy, "whole-pack", `${label} did not retain complete Pack retrieval.`);
assertEqual(state.remoteConfigurationCount, 1, `${label} did not retain exactly one remote profile.`);
const connection = parseWebDAVConnectionURI(state.webDAVactiveConnectionURI);
assertEqual(connection.endpoint, webDAV.endpoint, `${label} retained a different WebDAV endpoint.`);
assertEqual(connection.username, webDAV.username, `${label} retained a different WebDAV username.`);
assertEqual(connection.password, webDAV.password, `${label} retained a different WebDAV password.`);
assertEqual(connection.prefix, prefix, `${label} retained a different WebDAV prefix.`);
assertEqual(
connection.useCustomRequestHandler,
true,
`${label} did not retain the Obsidian internal request API selection.`
);
}
function deterministicBytes(length: number, seed: number): Uint8Array {
const bytes = new Uint8Array(length);
let state = seed;
for (let index = 0; index < bytes.byteLength; index += 1) {
state ^= state << 13;
state ^= state >>> 17;
state ^= state << 5;
bytes[index] = state & 0xff;
}
return bytes;
}
async function writePayloadViaObsidian(
cliBinary: string,
environment: NodeJS.ProcessEnv,
text: string,
binarySeed: number
): Promise<void> {
await evalObsidianJson<unknown>(
cliBinary,
[
"(async()=>{",
`const textPath=${JSON.stringify(textPath)};`,
`const binaryPath=${JSON.stringify(binaryPath)};`,
`const text=${JSON.stringify(text)};`,
`const binaryLength=${JSON.stringify(binaryLength)};`,
`let state=${JSON.stringify(binarySeed)};`,
"let folder='';",
"for(const part of textPath.split('/').slice(0,-1)){",
"folder=folder?`${folder}/${part}`:part;",
"if(!(await app.vault.adapter.exists(folder))) await app.vault.createFolder(folder);",
"}",
"const existingText=app.vault.getAbstractFileByPath(textPath);",
"if(existingText) await app.vault.modify(existingText,text);",
"else await app.vault.create(textPath,text);",
"const bytes=new Uint8Array(binaryLength);",
"for(let i=0;i<bytes.byteLength;i++){",
"state^=state<<13;state^=state>>>17;state^=state<<5;bytes[i]=state&0xff;",
"}",
"const existingBinary=app.vault.getAbstractFileByPath(binaryPath);",
"if(existingBinary) await app.vault.modifyBinary(existingBinary,bytes.buffer);",
"else await app.vault.createBinary(binaryPath,bytes.buffer);",
"return JSON.stringify({ok:true});",
"})()",
].join(""),
environment
);
await waitForLocalDatabaseEntry(cliBinary, environment, textPath);
await waitForLocalDatabaseEntry(cliBinary, environment, binaryPath);
}
async function waitForText(vault: TemporaryVault, expected: string): Promise<void> {
const deadline = Date.now() + Number(process.env.E2E_OBSIDIAN_FILE_TIMEOUT_MS ?? 30000);
let lastContent = "";
while (Date.now() < deadline) {
try {
lastContent = await readFile(join(vault.path, textPath), "utf8");
if (lastContent === expected) return;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
await new Promise((resolve) => setTimeout(resolve, 250));
}
throw new Error(`Timed out waiting for ${textPath}. Last content:\n${lastContent}`);
}
async function waitForBinary(vault: TemporaryVault, expected: Uint8Array): Promise<void> {
const deadline = Date.now() + Number(process.env.E2E_OBSIDIAN_FILE_TIMEOUT_MS ?? 30000);
let lastLength = -1;
while (Date.now() < deadline) {
try {
const actual = await readFile(join(vault.path, binaryPath));
lastLength = actual.byteLength;
if (actual.byteLength === expected.byteLength && actual.equals(Buffer.from(expected))) return;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
await new Promise((resolve) => setTimeout(resolve, 250));
}
throw new Error(`Timed out waiting for ${binaryPath}; last length was ${lastLength}.`);
}
async function pushAndObserve(session: ObsidianLiveSyncSession, cliBinary: string): Promise<number> {
const before = await waitForRemoteActivityState(session.remoteDebuggingPort, REMOTE_ACTIVITY_EXPECTED_STATE.idle);
await pushLocalChanges(cliBinary, session.cliEnv);
const after = await waitForRemoteActivityState(session.remoteDebuggingPort, REMOTE_ACTIVITY_EXPECTED_STATE.idle);
if (after.requestCount <= before.requestCount) {
throw new Error("Adaptive WebDAV synchronisation did not advance the tracked remote-request count.");
}
assertEqual(
after.responseCount,
after.requestCount,
"Adaptive WebDAV remote-request counters did not rebalance after synchronisation."
);
return after.requestCount - before.requestCount;
}
async function waitForAdaptiveObjects(
webDAV: WebDAVConfig,
prefix: string,
minimumWriters: number,
minimumCommits: number
): Promise<string[]> {
const deadline = Date.now() + remoteTimeoutMs;
let keys: string[] = [];
while (Date.now() < deadline) {
keys = await listWebDAVObjectKeys(webDAV, prefix);
const writerCount = keys.filter((key) => key.startsWith("a1~writer~")).length;
const commitCount = keys.filter((key) => key.startsWith("a1~commit~")).length;
if (keys.includes("a1~manifest.json") && writerCount >= minimumWriters && commitCount >= minimumCommits) {
return keys;
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
throw new Error(
`Timed out waiting for ${minimumWriters} Adaptive Writer object(s) and ${minimumCommits} Commit Bundle(s). Last keys: ${keys.join(", ")}`
);
}
async function assertAdaptiveRepository(webDAV: WebDAVConfig, prefix: string, keys: string[]): Promise<void> {
for (const legacyPrefix of ["a1~delta~", "a1~index~", "a1~metadata~"]) {
if (keys.some((key) => key.startsWith(legacyPrefix))) {
throw new Error(`Adaptive WebDAV wrote a retired object family ${legacyPrefix}: ${keys.join(", ")}`);
}
}
if (keys.some((key) => key.startsWith("a1~probe~"))) {
throw new Error(`Adaptive WebDAV left safety probe objects behind: ${keys.join(", ")}`);
}
if (keys.includes("_00000000-milestone.json")) {
throw new Error("Adaptive WebDAV wrote the Opaque Journal milestone.");
}
const manifest = JSON.parse(await readWebDAVObjectText(webDAV, prefix, "a1~manifest.json")) as {
format?: unknown;
formatVersion?: unknown;
manifestAuth?: unknown;
objectLayout?: unknown;
repositoryId?: unknown;
};
assertEqual(manifest.format, "adaptive-journal", "Unexpected Adaptive WebDAV manifest format.");
assertEqual(manifest.formatVersion, 1, "Unexpected Adaptive WebDAV manifest version.");
assertEqual(manifest.objectLayout, "commit-bundle-v1", "Unexpected Adaptive WebDAV object layout.");
if (typeof manifest.repositoryId !== "string" || manifest.repositoryId.length === 0) {
throw new Error("Adaptive WebDAV manifest did not contain a repository ID.");
}
if (typeof manifest.manifestAuth !== "string" || manifest.manifestAuth.length === 0) {
throw new Error("Adaptive WebDAV manifest did not contain its authentication value.");
}
}
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 webDAV = await loadWebDAVConfig();
const prefix = makeUniqueWebDAVPrefix("adaptive-obsidian");
const vaultPassphrase = randomBytes(24).toString("base64url");
const vaultA = await createTemporaryVault();
const vaultB = await createTemporaryVault();
const context: RunnerContext = { binary, cliBinary: cli.binary, activeSessions: new Set() };
const screenshots: string[] = [];
let generatedSetup: SetupArtifact | undefined;
let shouldStopWebDAV = false;
let observedRequests = 0;
try {
if (manageWebDAV) {
await runNpmScript("test:docker-webdav:start");
shouldStopWebDAV = !keepWebDAVFixture;
}
await assertWebDAVReachable(webDAV);
console.log(`Using Obsidian executable: ${binary}`);
console.log(`Temporary Vault A: ${vaultA.path}`);
console.log(`Temporary Vault B: ${vaultB.path}`);
console.log(`Temporary WebDAV target: ${webDAV.endpoint}/${prefix}`);
let session = await startSession(context, vaultA);
try {
screenshots.push(
...(await enterManualAdaptiveWebDAVSettings(
session.remoteDebuggingPort,
webDAV,
prefix,
vaultPassphrase
))
);
screenshots.push(await captureAndStartInitialisation(session.remoteDebuggingPort, "new", captures));
screenshots.push(await confirmRebuild(session.remoteDebuggingPort, captures));
screenshots.push(await skipMissingRemoteConfiguration(session.remoteDebuggingPort, captures));
screenshots.push(await acknowledgeDisabledOptionalFeatures(session.remoteDebuggingPort, captures));
const state = await finishInitialisation(session.remoteDebuggingPort, context.cliBinary, session.cliEnv);
await resumeCompatibilityReviewIfShown(session.remoteDebuggingPort);
assertAdaptiveWebDAVSettings(state, webDAV, prefix, "The first device");
await writePayloadViaObsidian(context.cliBinary, session.cliEnv, firstText, firstBinarySeed);
observedRequests += await pushAndObserve(session, context.cliBinary);
const keys = await waitForAdaptiveObjects(webDAV, prefix, 1, 1);
await assertAdaptiveRepository(webDAV, prefix, keys);
} catch (error) {
await captureFailure(session, "first-device");
throw error;
} finally {
await stopSession(context, session);
}
session = await startSession(context, vaultA);
try {
await waitForLiveSyncCoreReady(context.cliBinary, session.cliEnv);
await resumeCompatibilityReviewIfShown(session.remoteDebuggingPort);
const state = await readSetupState(context.cliBinary, session.cliEnv);
assertAdaptiveWebDAVSettings(state, webDAV, prefix, "The restarted first device");
const generated = await generateSetupURIFromDevice(
session.remoteDebuggingPort,
randomBytes(24).toString("base64url"),
captures
);
generatedSetup = generated.artifact;
screenshots.push(...generated.screenshots);
} catch (error) {
await captureFailure(session, "first-device-restart");
throw error;
} finally {
await stopSession(context, session);
}
session = await startSession(context, vaultB);
try {
if (!generatedSetup) throw new Error("The first device did not generate a Setup URI.");
screenshots.push(
await enterSetupURI(session.remoteDebuggingPort, "existing", generatedSetup, secondDeviceCaptures)
);
screenshots.push(
await captureAndStartInitialisation(session.remoteDebuggingPort, "existing", secondDeviceCaptures)
);
screenshots.push(...(await confirmFastFetch(session.remoteDebuggingPort, secondDeviceCaptures)));
// Journal remotes do not expose the legacy remote-configuration
// document. The device-generated Setup URI is the authoritative
// connection input, so acknowledge its expected absence explicitly.
screenshots.push(await skipMissingRemoteConfiguration(session.remoteDebuggingPort, secondDeviceCaptures));
const state = await finishInitialisation(session.remoteDebuggingPort, context.cliBinary, session.cliEnv);
await resumeCompatibilityReviewIfShown(session.remoteDebuggingPort);
assertAdaptiveWebDAVSettings(state, webDAV, prefix, "The second device");
observedRequests += await pushAndObserve(session, context.cliBinary);
await waitForText(vaultB, firstText);
await waitForBinary(vaultB, deterministicBytes(binaryLength, firstBinarySeed));
await writePayloadViaObsidian(context.cliBinary, session.cliEnv, secondText, secondBinarySeed);
observedRequests += await pushAndObserve(session, context.cliBinary);
const keys = await waitForAdaptiveObjects(webDAV, prefix, 2, 2);
await assertAdaptiveRepository(webDAV, prefix, keys);
} catch (error) {
await captureFailure(session, "second-device");
throw error;
} finally {
await stopSession(context, session);
}
session = await startSession(context, vaultA);
try {
await waitForLiveSyncCoreReady(context.cliBinary, session.cliEnv);
await resumeCompatibilityReviewIfShown(session.remoteDebuggingPort);
assertAdaptiveWebDAVSettings(
await readSetupState(context.cliBinary, session.cliEnv),
webDAV,
prefix,
"The final first-device session"
);
observedRequests += await pushAndObserve(session, context.cliBinary);
await waitForText(vaultA, secondText);
await waitForBinary(vaultA, deterministicBytes(binaryLength, secondBinarySeed));
} catch (error) {
await captureFailure(session, "return-journey");
throw error;
} finally {
await stopSession(context, session);
}
console.log(
`Adaptive WebDAV passed visible safety-gated onboarding, restart persistence, a device-generated Setup URI, and a two-device text and binary return journey. Tracked requests across measured synchronisations: ${observedRequests}. Screenshots: ${screenshots.join(", ")}`
);
} finally {
await stopSessions(context).catch((error: unknown) => {
console.warn(error instanceof Error ? error.message : error);
});
await vaultA.dispose();
await vaultB.dispose();
if (process.env.E2E_OBSIDIAN_KEEP_WEBDAV !== "true") {
await deleteWebDAVPrefix(webDAV, prefix).catch((error: unknown) => {
console.warn(error instanceof Error ? error.message : error);
});
}
if (shouldStopWebDAV) {
await runNpmScript("test:docker-webdav:stop").catch((error: unknown) => {
console.warn(error instanceof Error ? error.message : error);
});
}
}
}
main().catch((error: unknown) => {
console.error(error instanceof Error ? error.stack : error);
process.exit(1);
});
+20
View File
@@ -31,6 +31,7 @@ const testSteps: Step[] = [
args: ["run", "test:e2e:obsidian:cli-to-obsidian-sync"],
},
{ name: "Object Storage upload", args: ["run", "test:e2e:obsidian:minio-upload"] },
{ name: "Adaptive WebDAV workflow", args: ["run", "test:e2e:obsidian:adaptive-webdav"] },
{
name: "Object Storage Setup URI workflow",
args: ["run", "test:e2e:obsidian:object-storage-setup-uri-workflow"],
@@ -47,10 +48,12 @@ const testSteps: Step[] = [
const manageCouchDb = process.argv.includes("--manage-couchdb") || process.argv.includes("--manage-services");
const manageMinio = process.argv.includes("--manage-minio") || process.argv.includes("--manage-services");
const manageP2P = process.argv.includes("--manage-p2p") || process.argv.includes("--manage-services");
const manageWebDAV = process.argv.includes("--manage-webdav") || process.argv.includes("--manage-services");
const keepServices = process.argv.includes("--keep-services");
const keepCouchDb = keepServices || process.argv.includes("--keep-couchdb");
const keepMinio = keepServices || process.argv.includes("--keep-minio");
const keepP2P = keepServices || process.argv.includes("--keep-p2p");
const keepWebDAV = keepServices || process.argv.includes("--keep-webdav");
function npmBinary(): string {
return process.platform === "win32" ? "npm.cmd" : "npm";
@@ -105,10 +108,19 @@ async function stopManagedP2P(): Promise<void> {
});
}
async function stopManagedWebDAV(): Promise<void> {
await runStep({
name: "stop WebDAV fixture",
args: ["run", "test:docker-webdav:stop"],
optional: true,
});
}
async function main(): Promise<void> {
let shouldStopCouchDb = false;
let shouldStopMinio = false;
let shouldStopP2P = false;
let shouldStopWebDAV = false;
try {
if (manageCouchDb) {
await stopManagedCouchDb();
@@ -125,11 +137,19 @@ async function main(): Promise<void> {
await runStep({ name: "start P2P relay fixture", args: ["run", "test:docker-p2p:start"] });
shouldStopP2P = !keepP2P;
}
if (manageWebDAV) {
await stopManagedWebDAV();
await runStep({ name: "start WebDAV fixture", args: ["run", "test:docker-webdav:start"] });
shouldStopWebDAV = !keepWebDAV;
}
for (const step of testSteps) {
await runStep(step);
}
} finally {
if (shouldStopWebDAV) {
await stopManagedWebDAV();
}
if (shouldStopP2P) {
await stopManagedP2P();
}
+2 -1
View File
@@ -19,6 +19,7 @@ const focusedScenarios = new Set([
"couchdb-manual-setup-workflow",
"cli-to-obsidian-sync",
"minio-upload",
"adaptive-webdav",
"object-storage-setup-uri-workflow",
"p2p-setup-uri-workflow",
"startup-scan",
@@ -39,7 +40,7 @@ real-Obsidian scenario. Supported scenarios:
${[...focusedScenarios].map((scenario) => ` ${scenario}`).join("\n")}
This wrapper does not start CouchDB, Object Storage, or the P2P signalling
This wrapper does not start CouchDB, Object Storage, WebDAV, or the P2P signalling
relay. Use the documented service commands or the complete
local-suite:services wrapper when required.`;
}