Integrate content evidence for unchanged files

This commit is contained in:
vorotamoroz committed 2026-10-17 14:14:06 +00:00
commit 6f2db09ca9
21 files changed
+565 -23

No files matched your search

@@ -102,6 +102,7 @@ export function initialiseServiceModulesCLI(
events: services.context.events,
API: services.API,
database: services.database,
path: services.path,
appLifecycle: services.appLifecycle,
setting: services.setting,
remote: services.remote,
@@ -93,6 +93,7 @@ export function initialiseServiceModulesFSAPI(
events: services.context.events,
API: services.API,
database: services.database,
path: services.path,
appLifecycle: services.appLifecycle,
setting: services.setting,
remote: services.remote,
@@ -37,6 +37,14 @@ export const REVIEW_HARNESS_SCENARIOS = [
mode: "automatic",
access: "read-only",
},
{
id: "file-content-hash-performance",
title: "File content hash performance",
description:
"Compares SHA-256 and xxHash64 on fixed in-memory Blob inputs of 4 KiB, 64 KiB, 1 MiB, and 50 MiB. Input construction is excluded from hash timings; engine initialisation and warm-up are reported separately. Keep Obsidian in the foreground.",
mode: "manual",
access: "read-only",
},
] as const;
export const REVIEW_HARNESS_SCENARIO_IDS = REVIEW_HARNESS_SCENARIOS.map(({ id }) => id);
@@ -2,6 +2,7 @@ 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_SCENARIOS,
REVIEW_HARNESS_SCENARIO_IDS,
formatReviewHarnessReport,
inspectCompatibilityReview,
@@ -76,7 +77,12 @@ describe("Review Harness contract", () => {
"compatibility-review",
"vault-round-trip",
"id-generation-performance",
"file-content-hash-performance",
]);
expect(REVIEW_HARNESS_SCENARIOS.find(({ id }) => id === "file-content-hash-performance")).toMatchObject({
mode: "manual",
access: "read-only",
});
});
it("checks that an existing Vault exposes the seven synchronisation choices as booleans", () => {
@@ -22,6 +22,7 @@ export interface ReviewHarnessRuntime {
openCompatibilityReview(): Promise<void>;
runVaultRoundTrip(): Promise<ReviewHarnessScenarioResult>;
runIdBenchmark(): Promise<ReviewHarnessScenarioResult>;
runFileContentHashBenchmark(): Promise<ReviewHarnessScenarioResult>;
readContinuation(): string | null;
writeContinuation(value: string): void;
deleteContinuation(): void;
@@ -162,6 +163,8 @@ export class ReviewHarnessController {
result = await this.runtime.runVaultRoundTrip();
} else if (id === "id-generation-performance") {
result = await this.runtime.runIdBenchmark();
} else if (id === "file-content-hash-performance") {
result = await this.runtime.runFileContentHashBenchmark();
} else {
const inspection = this.inspectCompatibilityReview();
result =
@@ -85,6 +85,11 @@ function createRuntime(): ReviewHarnessRuntime & {
detail: "ID generation measurements completed.",
observations: ["Chunk 256 B: 1000 IDs total=43.00 ms; per ID=0.0430 ms"],
})),
runFileContentHashBenchmark: vi.fn(async () => ({
status: "passed" as const,
detail: "File content hash measurements completed.",
observations: ["SHA-256, 4 KiB: median=1.00 ms; range=1.00–1.00 ms"],
})),
readContinuation() {
return this.continuation;
},
@@ -121,6 +126,7 @@ describe("ReviewHarnessController", () => {
expect(controller.snapshot().results["settings-lifecycle"].status).toBe("passed");
expect(controller.snapshot().results["vault-round-trip"].status).toBe("idle");
expect(controller.snapshot().results["compatibility-review"].status).toBe("idle");
expect(controller.snapshot().results["file-content-hash-performance"].status).toBe("idle");
});
it("runs the Vault fixture scenario only when it is selected explicitly", async () => {
@@ -185,6 +191,42 @@ describe("ReviewHarnessController", () => {
expect(runtime.reportError).toHaveBeenCalledOnce();
});
it("runs file content hash measurements on request without running them automatically", async () => {
const runtime = createRuntime();
const controller = new ReviewHarnessController(runtime);
await controller.runAutomaticScenarios();
expect(runtime.runFileContentHashBenchmark).not.toHaveBeenCalled();
await controller.runScenario("file-content-hash-performance");
expect(runtime.runFileContentHashBenchmark).toHaveBeenCalledOnce();
expect(controller.snapshot().results["file-content-hash-performance"]).toMatchObject({ status: "passed" });
expect(runtime.runVaultRoundTrip).not.toHaveBeenCalled();
});
it("includes file content hash measurements in Full review", async () => {
const runtime = createRuntime();
const controller = new ReviewHarnessController(runtime);
await controller.runAllScenarios();
expect(runtime.runFileContentHashBenchmark).toHaveBeenCalledOnce();
expect(controller.snapshot().results["file-content-hash-performance"].status).toBe("passed");
});
it("excludes an unexpected file content hash error from the copied report", async () => {
const runtime = createRuntime();
runtime.runFileContentHashBenchmark = vi.fn().mockRejectedValue(new Error("private benchmark content"));
const controller = new ReviewHarnessController(runtime);
await controller.runScenario("file-content-hash-performance");
expect(controller.snapshot().results["file-content-hash-performance"].status).toBe("failed");
expect(controller.createReport()).not.toContain("private benchmark content");
expect(runtime.reportError).toHaveBeenCalledOnce();
});
it("does not overlap an ID measurement with another scenario", async () => {
const runtime = createRuntime();
let finish!: () => void;
@@ -0,0 +1,149 @@
import type { ReviewHarnessScenarioResult } from "./reviewHarnessTypes";
export const REVIEW_HARNESS_FILE_CONTENT_HASH_INPUTS = [
{ label: "4 KiB", bytes: 4 * 1024 },
{ label: "64 KiB", bytes: 64 * 1024 },
{ label: "1 MiB", bytes: 1024 * 1024 },
{ label: "50 MiB", bytes: 50 * 1024 * 1024 },
] as const;
const CONTENT_BLOCK_BYTES = 64 * 1024;
const SAMPLE_COUNT = 3;
export interface FileContentHashBenchmarkOperations {
readonly xxHashInitialisationMs: number;
createInput(bytes: number): Blob;
sha256(content: Blob): Promise<`sha256:${string}`>;
xxHash64(content: Blob): Promise<bigint>;
}
type BenchmarkPerformance = Pick<Performance, "now"> & {
readonly memory?: { readonly usedJSHeapSize: number };
};
type HashAlgorithm = "SHA-256" | "xxHash64";
function readHeap(clock: BenchmarkPerformance): number | undefined {
try {
const bytes = clock.memory?.usedJSHeapSize;
return typeof bytes === "number" && Number.isFinite(bytes) && bytes >= 0 ? bytes : undefined;
} catch {
return undefined;
}
}
function summary(samples: readonly number[]): string {
const sorted = [...samples].sort((a, b) => a - b);
const middle = sorted[Math.floor(sorted.length / 2)];
return `median=${middle.toFixed(2)} ms; range=${sorted[0].toFixed(2)}–${sorted[sorted.length - 1].toFixed(2)} ms`;
}
function hashKey(hash: `sha256:${string}` | bigint): string {
if (typeof hash === "bigint") return hash.toString(16);
return hash;
}
export function createFixedFileContentHashInput(bytes: number): Blob {
const block = new Uint8Array(CONTENT_BLOCK_BYTES);
for (let index = 0; index < block.length; index++) block[index] = (index * 31 + 17) & 0xff;
const parts: BlobPart[] = [];
for (let remaining = bytes; remaining > 0; remaining -= CONTENT_BLOCK_BYTES) {
parts.push(block.subarray(0, Math.min(remaining, CONTENT_BLOCK_BYTES)));
}
return new Blob(parts);
}
export async function runReviewHarnessFileContentHashBenchmark(
operations: FileContentHashBenchmarkOperations,
clock: BenchmarkPerformance = performance,
yieldControl: () => Promise<void> = () => new Promise((resolve) => window.setTimeout(resolve, 0))
): Promise<ReviewHarnessScenarioResult> {
if (!Number.isFinite(operations.xxHashInitialisationMs) || operations.xxHashInitialisationMs < 0) {
throw new Error("The xxHash64 initialisation time is invalid.");
}
const before = readHeap(clock);
let highest = before;
const sampleHeap = () => {
const value = readHeap(clock);
if (value !== undefined) highest = Math.max(highest ?? value, value);
return value;
};
const inputs: { readonly label: string; readonly bytes: number; readonly content: Blob }[] = [];
for (const { label, bytes } of REVIEW_HARNESS_FILE_CONTENT_HASH_INPUTS) {
await yieldControl();
inputs.push({ label, bytes, content: operations.createInput(bytes) });
sampleHeap();
}
const hash = (algorithm: HashAlgorithm, content: Blob) =>
algorithm === "SHA-256" ? operations.sha256(content) : operations.xxHash64(content);
const warmupTimes = new Map<HashAlgorithm, number>();
const signatures = new Map<string, string>();
const checkHash = (algorithm: HashAlgorithm, label: string, result: `sha256:${string}` | bigint): void => {
const value = hashKey(result);
const key = `${algorithm}:${label}`;
const previous = signatures.get(key);
if (previous !== undefined && previous !== value) {
throw new Error(`The ${algorithm} result changed for fixed in-memory content.`);
}
signatures.set(key, value);
};
for (const algorithm of ["SHA-256", "xxHash64"] as const) {
await yieldControl();
const started = clock.now();
const result = await hash(algorithm, inputs[0].content);
const elapsed = clock.now() - started;
checkHash(algorithm, inputs[0].label, result);
warmupTimes.set(algorithm, elapsed);
sampleHeap();
}
const observations = [
"Fixed in-memory Blob inputs contain the same deterministic byte sequence at 4 KiB, 64 KiB, 1 MiB, and 50 MiB. Input construction is excluded from timings.",
`xxHash64 engine initialisation: ${operations.xxHashInitialisationMs.toFixed(2)} ms; measured separately.`,
`Warm-up on 4 KiB, one call per algorithm: SHA-256=${warmupTimes.get("SHA-256")!.toFixed(2)} ms; xxHash64=${warmupTimes.get("xxHash64")!.toFixed(2)} ms; excluded from samples.`,
"Each algorithm has three samples per input. SHA-256 uses the Self-hosted LiveSync file content hash helper; xxHash64 timings include Blob.arrayBuffer() and h64Raw. Calls run serially, with alternating algorithm order and a UI yield between calls.",
];
for (const input of inputs) {
const samples: Record<HashAlgorithm, number[]> = { "SHA-256": [], xxHash64: [] };
for (let sample = 0; sample < SAMPLE_COUNT; sample++) {
const order: readonly HashAlgorithm[] =
sample % 2 === 0 ? ["SHA-256", "xxHash64"] : ["xxHash64", "SHA-256"];
for (const algorithm of order) {
await yieldControl();
const started = clock.now();
const result = await hash(algorithm, input.content);
const elapsed = clock.now() - started;
checkHash(algorithm, input.label, result);
if (!Number.isFinite(elapsed) || elapsed < 0) {
throw new Error(`The ${algorithm} measurement returned an invalid duration.`);
}
samples[algorithm].push(elapsed);
sampleHeap();
}
}
for (const algorithm of ["SHA-256", "xxHash64"] as const) {
observations.push(`${algorithm}, ${input.label}: ${summary(samples[algorithm])}.`);
}
}
const after = sampleHeap();
if (highest === undefined) {
observations.push("JavaScript heap: unavailable on this device.");
} else {
const mib = (bytes: number | undefined) =>
bytes === undefined ? "unavailable" : `${(bytes / 1048576).toFixed(2)} MiB`;
observations.push(
`JavaScript heap: before=${mib(before)}; highest sampled=${mib(highest)}; after=${mib(after)}.`
);
}
observations.push(
"Heap samples are approximate, may include other Obsidian work, and are affected by garbage collection. They are neither total app RAM nor a true peak."
);
return { status: "passed", detail: "File content hash measurements completed.", observations };
}
@@ -0,0 +1,134 @@
import { describe, expect, it } from "vitest";
import {
REVIEW_HARNESS_FILE_CONTENT_HASH_INPUTS,
createFixedFileContentHashInput,
runReviewHarnessFileContentHashBenchmark,
type FileContentHashBenchmarkOperations,
} from "./reviewHarnessFileContentHashBenchmark";
function fixture() {
let elapsed = 0;
let activeCalls = 0;
let maximumConcurrentCalls = 0;
let yieldCount = 0;
const contentSizes = new WeakMap<Blob, number>();
const contentBySize = new Map<number, Blob>();
const callCounts = new Map<string, number>();
const observedInputs = new Map<string, Blob[]>();
const operations: FileContentHashBenchmarkOperations = {
xxHashInitialisationMs: 12.5,
createInput: (bytes) => {
elapsed += 10000;
const content = new Blob([new Uint8Array(4)]);
contentSizes.set(content, bytes);
contentBySize.set(bytes, content);
return content;
},
sha256: async (content) => {
await measure("SHA-256", content);
return `sha256:${contentSizes.get(content)!}`;
},
xxHash64: async (content) => {
await measure("xxHash64", content);
return BigInt(contentSizes.get(content)!);
},
};
async function measure(algorithm: string, content: Blob): Promise<void> {
activeCalls++;
maximumConcurrentCalls = Math.max(maximumConcurrentCalls, activeCalls);
const bytes = contentSizes.get(content)!;
const key = `${algorithm}:${bytes}`;
const count = callCounts.get(key) ?? 0;
callCounts.set(key, count + 1);
observedInputs.set(key, [...(observedInputs.get(key) ?? []), content]);
const durations =
bytes === 4 * 1024
? algorithm === "SHA-256"
? [100, 1, 3, 5]
: [200, 11, 13, 15]
: algorithm === "SHA-256"
? [1, 3, 5]
: [11, 13, 15];
await Promise.resolve();
elapsed += durations[count];
activeCalls--;
}
return {
operations,
now: () => elapsed,
yieldControl: async () => {
yieldCount++;
await Promise.resolve();
},
inputs: () => ({ contentBySize, callCounts, observedInputs, yieldCount, maximumConcurrentCalls }),
};
}
describe("Review Harness file content hash measurements", () => {
it("measures fixed Blob inputs with separate initialisation, warm-up, and sample ranges", async () => {
const f = fixture();
const result = await runReviewHarnessFileContentHashBenchmark(f.operations, { now: f.now }, f.yieldControl);
const report = result.observations.join("\n");
expect(result.status).toBe("passed");
expect(result.detail).toBe("File content hash measurements completed.");
expect([...f.inputs().contentBySize.keys()]).toEqual(
REVIEW_HARNESS_FILE_CONTENT_HASH_INPUTS.map(({ bytes }) => bytes)
);
expect(Object.fromEntries(f.inputs().callCounts)).toEqual({
"SHA-256:4096": 4,
"xxHash64:4096": 4,
...Object.fromEntries(
REVIEW_HARNESS_FILE_CONTENT_HASH_INPUTS.slice(1).flatMap(({ bytes }) => [
[`SHA-256:${bytes}`, 3],
[`xxHash64:${bytes}`, 3],
])
),
});
expect(f.inputs().yieldCount).toBe(30);
expect(f.inputs().maximumConcurrentCalls).toBe(1);
for (const { bytes } of REVIEW_HARNESS_FILE_CONTENT_HASH_INPUTS) {
for (const algorithm of ["SHA-256", "xxHash64"]) {
expect(f.inputs().observedInputs.get(`${algorithm}:${bytes}`)?.every(
(content) => content === f.inputs().contentBySize.get(bytes)
)).toBe(true);
}
}
expect(report).toContain("xxHash64 engine initialisation: 12.50 ms; measured separately.");
expect(report).toContain(
"Warm-up on 4 KiB, one call per algorithm: SHA-256=100.00 ms; xxHash64=200.00 ms; excluded from samples."
);
expect(report).toContain("SHA-256, 4 KiB: median=3.00 ms; range=1.00–5.00 ms.");
expect(report).toContain("xxHash64, 4 KiB: median=13.00 ms; range=11.00–15.00 ms.");
expect(report).toContain("Each algorithm has three samples per input.");
expect(report).not.toContain("sha256:4096");
expect(report).not.toContain("4096n");
});
it("uses the same deterministic byte sequence for short and long inputs", async () => {
const shortInput = createFixedFileContentHashInput(4 * 1024);
const longInput = createFixedFileContentHashInput(64 * 1024);
expect(shortInput.size).toBe(4 * 1024);
expect(longInput.size).toBe(64 * 1024);
expect(new Uint8Array(await longInput.slice(0, shortInput.size).arrayBuffer())).toEqual(
new Uint8Array(await shortInput.arrayBuffer())
);
});
it("propagates hash failures without turning a partial run into a passed measurement", async () => {
const f = fixture();
const operations = {
...f.operations,
sha256: async () => {
throw new Error("synthetic hashing failure");
},
};
await expect(
runReviewHarnessFileContentHashBenchmark(operations, { now: f.now }, f.yieldControl)
).rejects.toThrow("synthetic hashing failure");
});
});
@@ -0,0 +1,20 @@
import { computeFileContentHash } from "@vrtmrz/livesync-commonlib/hashing";
import { xxhashNew } from "octagonal-wheels/hash/xxhash.js";
import {
createFixedFileContentHashInput,
type FileContentHashBenchmarkOperations,
} from "./reviewHarnessFileContentHashBenchmark";
export async function createFileContentHashBenchmarkOperations(
clock: Pick<Performance, "now"> = performance
): Promise<FileContentHashBenchmarkOperations> {
const started = clock.now();
const xxHash = await xxhashNew();
const xxHashInitialisationMs = clock.now() - started;
return {
xxHashInitialisationMs,
createInput: createFixedFileContentHashInput,
sha256: (content) => computeFileContentHash(content),
xxHash64: async (content) => xxHash.h64Raw(new Uint8Array(await content.arrayBuffer())),
};
}
@@ -0,0 +1,15 @@
import { describe, expect, it } from "vitest";
import { createFileContentHashBenchmarkOperations } from "./reviewHarnessFileContentHashBenchmarkRuntime";
describe("Review Harness file content hash benchmark adapter", () => {
it("uses the packaged SHA-256 helper and xxHash64 engine with fixed Blob content", async () => {
const operations = await createFileContentHashBenchmarkOperations();
const content = operations.createInput(4 * 1024);
expect(operations.xxHashInitialisationMs).toBeGreaterThanOrEqual(0);
expect(await operations.sha256(content)).toMatch(/^sha256:/u);
expect(typeof (await operations.xxHash64(content))).toBe("bigint");
expect(await operations.sha256(content)).toBe(await operations.sha256(content));
expect(await operations.xxHash64(content)).toBe(await operations.xxHash64(content));
});
});
+1
View File
@@ -115,6 +115,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
events: services.context.events,
API: services.API,
database: services.database,
path: services.path,
appLifecycle: services.appLifecycle,
setting: services.setting,
remote: services.remote,
+7
View File
@@ -17,6 +17,8 @@ import {
import type { CompatibilityReviewController } from "./compatibilityReview";
import { runReviewHarnessIdBenchmark } from "@/features/ReviewHarness/reviewHarnessIdBenchmark";
import { createIdBenchmarkOperations } from "@/features/ReviewHarness/reviewHarnessIdBenchmarkRuntime";
import { runReviewHarnessFileContentHashBenchmark } from "@/features/ReviewHarness/reviewHarnessFileContentHashBenchmark";
import { createFileContentHashBenchmarkOperations } from "@/features/ReviewHarness/reviewHarnessFileContentHashBenchmarkRuntime";
async function runVaultRoundTrip(plugin: ObsidianLiveSyncPlugin): Promise<ReviewHarnessScenarioResult> {
const vault = plugin.app.vault;
@@ -62,6 +64,11 @@ export function useReviewHarness(
runVaultRoundTrip: () => runVaultRoundTrip(plugin),
runIdBenchmark: async () =>
runReviewHarnessIdBenchmark(await createIdBenchmarkOperations(), activeWindow.performance),
runFileContentHashBenchmark: async () =>
runReviewHarnessFileContentHashBenchmark(
await createFileContentHashBenchmarkOperations(activeWindow.performance),
activeWindow.performance
),
readContinuation: () => services.setting.getSmallConfig(REVIEW_HARNESS_STATE_KEY),
writeContinuation: (value) => services.setting.setSmallConfig(REVIEW_HARNESS_STATE_KEY, value),
deleteContinuation: () => services.setting.deleteSmallConfig(REVIEW_HARNESS_STATE_KEY),
@@ -13,6 +13,7 @@ import {
type ServiceDatabaseFileAccessDependencies,
} from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceDatabaseFileAccessBase";
import type { ServiceFileHandlerDependencies } from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceFileHandlerBase";
import type { FileReflectionProvenanceRecord } from "@vrtmrz/livesync-commonlib/compat/interfaces/FileReflectionProvenance";
import { ServiceFileHandler } from "./FileHandler";
import {
createConflictResolutionOperations,
@@ -38,7 +39,7 @@ function makeFile(body: string, mtime = oldTime): UXFileInfo {
async function makeDevice(name: string) {
const db = new PouchDB<EntryDoc>(name, { adapter: "memory" });
const reflection = new Map<FilePathWithPrefix, { revision: string; observedStorageMtime?: number }>();
const reflection = new Map<FilePathWithPrefix, FileReflectionProvenanceRecord>();
let storage = makeFile(old);
const settings = { ...DEFAULT_SETTINGS, useOnlyLocalChunk: true, writeDocumentsIfConflicted: false };
const setting = { currentSettings: () => settings };
@@ -94,7 +95,7 @@ async function makeDevice(name: string) {
conflict,
fileReflectionProvenance: {
get: (value: FilePathWithPrefix) => Promise.resolve(reflection.get(value)),
set: (value: FilePathWithPrefix, record: { revision: string }) => {
set: (value: FilePathWithPrefix, record: FileReflectionProvenanceRecord) => {
reflection.set(value, record);
return Promise.resolve();
},
@@ -287,7 +288,32 @@ describe("file provenance across multiple devices and real CouchDB", () => {
}
}, 60_000);
it.each(["missing record", "compacted base"] as const)(
it("reflects the selected content after compacting an unchanged losing branch", async () => {
const f = await conflictedDevices(4);
const { keep, content } = await resolveAndReplicate(f);
const index = f.revisions.findIndex((rev, i) => i > 0 && rev !== keep);
const device = f.devices[index];
const record = device.reflection.get(path);
expect(record?.revision).toBe(f.revisions[index]);
expect(record?.contentHash).toMatch(/^sha256:[0-9a-f]{64}$/u);
expect(record?.contentSize).toBe(device.getStorage().body.size);
await device.db.compact();
await expect(device.db.get(path, { rev: f.revisions[index] })).rejects.toMatchObject({ status: 404 });
const before = (await device.db.info()).update_seq;
await expect(device.handler.storeFileToDB(path)).resolves.toBe(true);
expect((await device.db.info()).update_seq).toBe(before);
expect(await device.getStorage().body.text()).toBe(content);
expect(device.reflection.get(path)?.revision).toBe(keep);
await device.db.replicate.to(f.remote);
for (const peer of f.devices) {
await peer.db.replicate.from(f.remote);
expect((await leaves(peer.db)).map((doc) => doc._rev)).toEqual([keep]);
expect(await revisionContent(peer, keep)).toBe(content);
}
}, 60_000);
it.each(["missing record", "compacted legacy base", "compacted hashed base with local edits"] as const)(
"preserves uncertain storage as one independent conflict with four devices: %s",
async (reason) => {
const f = await conflictedDevices(4);
@@ -301,6 +327,17 @@ describe("file provenance across multiple devices and real CouchDB", () => {
} else {
await device.db.compact();
await expect(device.db.get(path, { rev: f.revisions[index] })).rejects.toMatchObject({ status: 404 });
if (reason === "compacted legacy base") {
// Revision-only records still require protection when their recorded body is unavailable.
const { revision, observedStorageMtime } = device.reflection.get(path)!;
device.reflection.set(path, { revision, observedStorageMtime });
} else {
expect(device.reflection.get(path)?.contentHash).toMatch(/^sha256:/u);
const original = device.getStorage();
const edited = (await original.body.text()).replace("Edited", "EDITED");
device.setStorage(makeFile(edited, original.stat.mtime));
expect(device.getStorage().body.size).toBe(original.body.size);
}
}
const uncertainContent = await device.getStorage().body.text();
await expect(device.handler.storeFileToDB(path)).resolves.toBe(true);