mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-28 22:37:08 +00:00
test(obsidian): cover adaptive S3 journal upload
This commit is contained in:
@@ -30,6 +30,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 S3 upload", args: ["run", "test:e2e:obsidian:adaptive-s3"] },
|
||||
{
|
||||
name: "Object Storage Setup URI workflow",
|
||||
args: ["run", "test:e2e:obsidian:object-storage-setup-uri-workflow"],
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
listObjectStorageObjects,
|
||||
loadObjectStorageConfig,
|
||||
makeUniqueBucketPrefix,
|
||||
readObjectStorageJson,
|
||||
} from "../runner/objectStorage.ts";
|
||||
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
|
||||
import { createTemporaryVault } from "../runner/vault.ts";
|
||||
@@ -40,9 +41,21 @@ import { REMOTE_ACTIVITY_EXPECTED_STATE, waitForRemoteActivityState } from "../r
|
||||
|
||||
process.env.E2E_OBSIDIAN_CLI_TIMEOUT_MS ??= "30000";
|
||||
|
||||
const notePath = "E2E/minio-upload.md";
|
||||
const adaptive = process.argv.includes("--adaptive");
|
||||
const unsupportedArguments = process.argv.slice(2).filter((argument) => argument !== "--adaptive");
|
||||
if (unsupportedArguments.length > 0) {
|
||||
throw new Error(`Unsupported Object Storage upload argument: ${unsupportedArguments.join(", ")}`);
|
||||
}
|
||||
const journalSettings = adaptive
|
||||
? {
|
||||
journalFormat: "adaptive-v1",
|
||||
packReadPolicy: "range",
|
||||
}
|
||||
: {};
|
||||
const scenarioName = adaptive ? "Adaptive S3" : "Object Storage";
|
||||
const notePath = adaptive ? "E2E/adaptive-s3-upload.md" : "E2E/minio-upload.md";
|
||||
const noteContent = [
|
||||
"# Object Storage upload from real Obsidian",
|
||||
`# ${scenarioName} upload from real Obsidian`,
|
||||
"",
|
||||
"This note is created through Obsidian and uploaded by Self-hosted LiveSync to S3-compatible Object Storage.",
|
||||
"The test is intentionally small, but it crosses the real Obsidian, Journal Sync, and AWS SDK boundary.",
|
||||
@@ -78,7 +91,7 @@ async function createNoteAndWaitForLocalDb(cliBinary: string, env: NodeJS.Proces
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForObjectStorageObjects(prefix: string): Promise<string[]> {
|
||||
async function waitForObjectStorageObjects(prefix: string, requiredKeyPrefix?: string): Promise<string[]> {
|
||||
const objectStorage = await loadObjectStorageConfig();
|
||||
const timeoutMs = Number(process.env.E2E_OBSIDIAN_OBJECT_STORAGE_TIMEOUT_MS ?? 20000);
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
@@ -86,12 +99,48 @@ async function waitForObjectStorageObjects(prefix: string): Promise<string[]> {
|
||||
while (Date.now() < deadline) {
|
||||
const objects = await listObjectStorageObjects(objectStorage, prefix);
|
||||
keys = objects.flatMap((object) => (object.Key ? [object.Key] : []));
|
||||
if (keys.length > 0) {
|
||||
if (keys.length > 0 && (!requiredKeyPrefix || keys.some((key) => key.startsWith(requiredKeyPrefix)))) {
|
||||
return keys;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
throw new Error(`Timed out waiting for Object Storage objects under ${prefix}. Last keys: ${keys.join(", ")}`);
|
||||
throw new Error(
|
||||
`Timed out waiting for Object Storage objects under ${prefix}${requiredKeyPrefix ? ` with prefix ${requiredKeyPrefix}` : ""}. Last keys: ${keys.join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
async function assertAdaptiveObjects(prefix: string, keys: string[]): Promise<void> {
|
||||
const objectStorage = await loadObjectStorageConfig();
|
||||
const manifestKey = `${prefix}a1~manifest.json`;
|
||||
const requiredPrefixes = [`${prefix}a1~writer~`, `${prefix}a1~commit~`];
|
||||
if (!keys.includes(manifestKey)) {
|
||||
throw new Error(`Adaptive Journal manifest is missing. Keys: ${keys.join(", ")}`);
|
||||
}
|
||||
for (const requiredPrefix of requiredPrefixes) {
|
||||
if (!keys.some((key) => key.startsWith(requiredPrefix))) {
|
||||
throw new Error(`Adaptive Journal object prefix ${requiredPrefix} is missing. Keys: ${keys.join(", ")}`);
|
||||
}
|
||||
}
|
||||
if (keys.includes(`${prefix}_00000000-milestone.json`)) {
|
||||
throw new Error("Adaptive Journal wrote the legacy Opaque Journal milestone.");
|
||||
}
|
||||
|
||||
const manifest = await readObjectStorageJson<{
|
||||
format?: unknown;
|
||||
formatVersion?: unknown;
|
||||
manifestAuth?: unknown;
|
||||
objectLayout?: unknown;
|
||||
repositoryId?: unknown;
|
||||
}>(objectStorage, manifestKey);
|
||||
assertEqual(manifest.format, "adaptive-journal", "Unexpected Adaptive Journal manifest format.");
|
||||
assertEqual(manifest.formatVersion, 1, "Unexpected Adaptive Journal manifest version.");
|
||||
assertEqual(manifest.objectLayout, "commit-bundle-v1", "Unexpected Adaptive Journal object layout.");
|
||||
if (typeof manifest.repositoryId !== "string" || manifest.repositoryId.length === 0) {
|
||||
throw new Error("Adaptive Journal manifest did not contain a repository ID.");
|
||||
}
|
||||
if (typeof manifest.manifestAuth !== "string" || manifest.manifestAuth.length === 0) {
|
||||
throw new Error("Adaptive Journal manifest did not contain its authentication value.");
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
@@ -102,7 +151,7 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
const objectStorage = await loadObjectStorageConfig();
|
||||
const bucketPrefix = makeUniqueBucketPrefix("minio-upload");
|
||||
const bucketPrefix = makeUniqueBucketPrefix(adaptive ? "adaptive-s3-upload" : "minio-upload");
|
||||
const vault = await createTemporaryVault();
|
||||
let session: ObsidianLiveSyncSession | undefined;
|
||||
|
||||
@@ -119,18 +168,26 @@ async function main(): Promise<void> {
|
||||
cliBinary: cli.binary,
|
||||
vault,
|
||||
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
|
||||
pluginData: createE2eObjectStoragePluginData({
|
||||
...objectStorage,
|
||||
bucketPrefix,
|
||||
}),
|
||||
pluginData: createE2eObjectStoragePluginData(
|
||||
{
|
||||
...objectStorage,
|
||||
bucketPrefix,
|
||||
},
|
||||
journalSettings
|
||||
),
|
||||
localStorageEntries: createE2eObsidianDeviceLocalState(vault.name),
|
||||
});
|
||||
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
|
||||
|
||||
const configured = await configureObjectStorage(cli.binary, session.cliEnv, {
|
||||
...objectStorage,
|
||||
bucketPrefix,
|
||||
});
|
||||
const configured = await configureObjectStorage(
|
||||
cli.binary,
|
||||
session.cliEnv,
|
||||
{
|
||||
...objectStorage,
|
||||
bucketPrefix,
|
||||
},
|
||||
journalSettings
|
||||
);
|
||||
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
|
||||
assertEqual(configured.isConfigured, true, "Self-hosted LiveSync was not marked as configured.");
|
||||
assertEqual(configured.remoteType, "MINIO", "Remote type was not Object Storage.");
|
||||
@@ -138,6 +195,11 @@ async function main(): Promise<void> {
|
||||
assertEqual(configured.bucket, objectStorage.bucket, "Configured Object Storage bucket did not match.");
|
||||
assertEqual(configured.bucketPrefix, bucketPrefix, "Configured Object Storage bucket prefix did not match.");
|
||||
assertEqual(configured.liveSync, false, "LiveSync should remain disabled during this one-shot workflow.");
|
||||
if (adaptive) {
|
||||
assertEqual(configured.journalFormat, "adaptive-v1", "Adaptive Journal format was not retained.");
|
||||
assertEqual(configured.packReadPolicy, "range", "Adaptive Journal Pack retrieval was not retained.");
|
||||
assertEqual(configured.expectedRepositoryId, "", "A new Adaptive repository should not be pre-bound.");
|
||||
}
|
||||
|
||||
await prepareRemote(cli.binary, session.cliEnv);
|
||||
const activityBeforeUpload = await waitForRemoteActivityState(
|
||||
@@ -159,10 +221,16 @@ async function main(): Promise<void> {
|
||||
"Object Storage remote-request counters did not rebalance after synchronisation."
|
||||
);
|
||||
|
||||
const keys = await waitForObjectStorageObjects(bucketPrefix);
|
||||
const keys = await waitForObjectStorageObjects(
|
||||
bucketPrefix,
|
||||
adaptive ? `${bucketPrefix}a1~commit~` : undefined
|
||||
);
|
||||
if (adaptive) {
|
||||
await assertAdaptiveObjects(bucketPrefix, keys);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Uploaded ${localEntry.path} through Journal Sync to ${objectStorage.bucket}/${bucketPrefix} (${keys.length} object(s)); tracked requests: ${activityAfterUpload.requestCount - activityBeforeUpload.requestCount}`
|
||||
`Uploaded ${localEntry.path} through ${scenarioName} Journal Sync to ${objectStorage.bucket}/${bucketPrefix} (${keys.length} object(s)); tracked requests: ${activityAfterUpload.requestCount - activityBeforeUpload.requestCount}`
|
||||
);
|
||||
} finally {
|
||||
if (session) {
|
||||
|
||||
@@ -18,6 +18,7 @@ const focusedScenarios = new Set([
|
||||
"couchdb-manual-setup-workflow",
|
||||
"cli-to-obsidian-sync",
|
||||
"minio-upload",
|
||||
"adaptive-s3",
|
||||
"object-storage-setup-uri-workflow",
|
||||
"p2p-setup-uri-workflow",
|
||||
"startup-scan",
|
||||
|
||||
Reference in New Issue
Block a user